use memmap2::{Mmap, MmapOptions};
use std::alloc::{Layout, alloc_zeroed, dealloc};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::ptr::NonNull;
use std::slice;
pub const ARCH_ALIGNMENT: usize = 16;
#[derive(Debug)]
pub enum MappedFile<'a> {
Mmap(Mmap),
Owned { ptr: NonNull<u8>, layout: Layout },
Borrowed(&'a [u8]),
Empty,
}
unsafe impl Send for MappedFile<'_> {}
unsafe impl Sync for MappedFile<'_> {}
impl<'a> MappedFile<'a> {
pub fn allocate(size: usize, align: usize) -> io::Result<Self> {
if size == 0 {
return Ok(Self::Empty);
}
let layout = Layout::from_size_align(size, align)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let ptr = unsafe { alloc_zeroed(layout) };
let ptr = NonNull::new(ptr)
.ok_or_else(|| io::Error::new(io::ErrorKind::OutOfMemory, "allocation failed"))?;
Ok(Self::Owned { ptr, layout })
}
pub fn allocate_type<T>(count: usize) -> io::Result<Self> {
let size = size_of::<T>().checked_mul(count).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "allocation size overflows")
})?;
Self::allocate(size, align_of::<T>())
}
#[inline]
pub fn borrow(data: &'a [u8]) -> Self {
if data.is_empty() {
Self::Empty
} else {
Self::Borrowed(data)
}
}
pub fn map_from_file(file: &File, pos: u64, size: usize) -> io::Result<Self> {
if size == 0 {
return Ok(Self::Empty);
}
let mmap = unsafe { MmapOptions::new().offset(pos).len(size).map(file)? };
Ok(Self::Mmap(mmap))
}
pub fn map_or_read(file: &mut File, memorymap: bool, size: usize) -> io::Result<Self> {
let pos = file.stream_position()?;
if memorymap
&& pos % ARCH_ALIGNMENT as u64 == 0
&& let Ok(mapped) = Self::map_from_file(file, pos, size)
{
file.seek(SeekFrom::Start(pos + size as u64))?;
return Ok(mapped);
}
let mut owned = Self::allocate(size, ARCH_ALIGNMENT)?;
let buf = owned
.as_mut_slice()
.expect("a freshly allocated region is writable");
file.read_exact(buf)?;
Ok(owned)
}
#[inline]
pub fn as_mut_slice(&mut self) -> Option<&mut [u8]> {
match self {
Self::Owned { ptr, layout } => {
Some(unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), layout.size()) })
}
Self::Empty => Some(&mut []),
_ => None,
}
}
}
impl AsRef<[u8]> for MappedFile<'_> {
#[inline]
fn as_ref(&self) -> &[u8] {
match self {
Self::Mmap(mmap) => mmap.as_ref(),
Self::Owned { ptr, layout } => unsafe {
slice::from_raw_parts(ptr.as_ptr(), layout.size())
},
Self::Borrowed(slice) => slice,
Self::Empty => &[],
}
}
}
impl std::ops::Deref for MappedFile<'_> {
type Target = [u8];
#[inline]
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl Drop for MappedFile<'_> {
fn drop(&mut self) {
if let Self::Owned { ptr, layout } = self {
unsafe { dealloc(ptr.as_ptr(), *layout) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn temp_file_of(contents: &[u8]) -> NamedTempFile {
let mut file = NamedTempFile::new().unwrap();
file.write_all(contents).unwrap();
file.flush().unwrap();
file
}
#[test]
fn borrowed_regions_are_passed_through() {
let data = b"hello openfst";
assert_eq!(MappedFile::borrow(data).as_ref(), b"hello openfst");
}
#[test]
fn allocations_are_aligned_and_writable() {
let mut mapped = MappedFile::allocate(100, ARCH_ALIGNMENT).unwrap();
assert_eq!(mapped.len(), 100);
assert_eq!(mapped.as_ref().as_ptr() as usize % ARCH_ALIGNMENT, 0);
mapped.as_mut_slice().unwrap()[0] = 42;
assert_eq!(mapped[0], 42);
}
#[test]
fn allocations_start_zeroed() {
for size in [1, 16, 100, 4096] {
let mapped = MappedFile::allocate(size, ARCH_ALIGNMENT).unwrap();
assert!(
mapped.as_ref().iter().all(|&byte| byte == 0),
"size {size} came back non-zero"
);
}
}
#[test]
fn allocate_type_uses_the_types_alignment() {
let mapped = MappedFile::allocate_type::<u64>(8).unwrap();
assert_eq!(mapped.len(), 64);
assert_eq!(mapped.as_ref().as_ptr() as usize % align_of::<u64>(), 0);
}
#[test]
fn empty_regions_are_writable_and_zero_length() {
let mut mapped = MappedFile::allocate(0, ARCH_ALIGNMENT).unwrap();
assert!(mapped.is_empty());
assert_eq!(mapped.as_mut_slice(), Some(&mut [][..]));
let mut typed = MappedFile::allocate_type::<u32>(0).unwrap();
assert_eq!(typed.as_mut_slice(), Some(&mut [][..]));
}
#[test]
fn rejects_a_non_power_of_two_alignment() {
assert!(MappedFile::allocate(16, 3).is_err());
}
#[test]
fn maps_a_file_region() {
let file = temp_file_of(b"zero copy mapping test");
let mut handle = file.reopen().unwrap();
let mapped = MappedFile::map_or_read(&mut handle, true, 22).unwrap();
assert_eq!(mapped.as_ref(), b"zero copy mapping test");
assert_eq!(handle.stream_position().unwrap(), 22);
}
#[test]
fn maps_from_an_offset_that_is_not_page_aligned() {
let file = temp_file_of(&(0u8..=255).collect::<Vec<_>>());
let handle = file.reopen().unwrap();
let mapped = MappedFile::map_from_file(&handle, 16, 32).unwrap();
assert_eq!(mapped.as_ref(), &(16u8..48).collect::<Vec<_>>()[..]);
}
#[test]
fn falls_back_to_reading_at_an_unaligned_offset() {
let file = temp_file_of(b"0123456789abcdefghijklmnop");
let mut handle = file.reopen().unwrap();
handle.seek(SeekFrom::Start(3)).unwrap();
let mapped = MappedFile::map_or_read(&mut handle, true, 10).unwrap();
assert!(matches!(mapped, MappedFile::Owned { .. }));
assert_eq!(mapped.as_ref(), b"3456789abc");
assert_eq!(handle.stream_position().unwrap(), 13);
}
#[test]
fn reading_is_used_when_mapping_is_not_requested() {
let file = temp_file_of(b"read me please");
let mut handle = file.reopen().unwrap();
let mapped = MappedFile::map_or_read(&mut handle, false, 7).unwrap();
assert!(matches!(mapped, MappedFile::Owned { .. }));
assert_eq!(mapped.as_ref(), b"read me");
assert_eq!(handle.stream_position().unwrap(), 7);
}
#[test]
fn reading_past_the_end_of_the_file_fails() {
let file = temp_file_of(b"short");
let mut handle = file.reopen().unwrap();
assert!(MappedFile::map_or_read(&mut handle, false, 100).is_err());
}
}