You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
collect/src/memfile.rs

402 lines
11 KiB

//! Memory file handling
use super::*;
use std::os::unix::io::*;
use std::{
mem,
ops,
fs,
io,
path::Path,
borrow::{
Borrow,
BorrowMut,
},
};
pub mod fd;
pub mod error;
mod map;
/// Flags passed to `memfd_create()` when used in this module
const MEMFD_CREATE_FLAGS: libc::c_uint = libc::MFD_CLOEXEC;
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(transparent)]
pub struct RawFile(fd::RawFileDescriptor);
/// Attempt to get the length of a stream's file descriptor
#[inline]
#[cfg_attr(feature="logging", instrument(level="debug", err, skip_all, fields(from_fd = from.as_raw_fd())))]
pub fn stream_len(from: &(impl AsRawFd + ?Sized)) -> io::Result<u64>
{
let mut stat = std::mem::MaybeUninit::uninit();
match unsafe { libc::fstat(from.as_raw_fd(), stat.as_mut_ptr()) } {
-1 => Err(io::Error::last_os_error()),
_ => {
let stat = unsafe { stat.assume_init() };
debug_assert!(stat.st_size >= 0, "bad stat size");
Ok(stat.st_size as u64)
},
}
}
/// Create an in-memory `File`, with an optional name
#[cfg_attr(feature="logging", instrument(level="info", err))]
pub fn create_memfile(name: Option<&str>, size: usize) -> eyre::Result<fs::File>
{
if_trace!(debug!("Attempting to allocate {size} bytes of contiguous physical memory for memory file named {:?}", name.unwrap_or("<unbound>")));
RawFile::open_mem(name, size).map(Into::into)
.wrap_err(eyre!("Failed to open in-memory file")
.with_section(move || format!("{:?}", name).header("Proposed name"))
.with_section(|| size.header("Requested physical memory buffer size")))
}
impl Clone for RawFile
{
#[inline]
fn clone(&self) -> Self {
self.try_clone().expect("failed to duplicate raw fd")
}
}
impl RawFile
{
/// Get the raw fd for this raw file
#[inline(always)]
pub const fn fileno(&self) -> &fd::RawFileDescriptor
{
&self.0//.clone_const()
}
#[inline(always)]
pub fn into_fileno(self) -> fd::RawFileDescriptor
{
// SAFETY: We know this is safe since we are just converting the released (valid) fd from `self`
unsafe {
fd::RawFileDescriptor::new_unchecked(self.into_raw_fd())
}
}
#[inline(always)]
pub unsafe fn from_fileno(fd: fd::RawFileDescriptor) -> Self
{
Self::from_raw_fd(fd.get())
}
#[inline(always)]
const fn take_ownership_of_unchecked(fd: RawFd) -> Self
{
//! **Internal**: Non-`unsafe` and `const` version of `take_ownership_of_raw_unchecked()`
//! : assumes `fd` is `>= 0`
//!
//! For use in `memfile` functions where `fd` has already been checked for validation (since `unsafe fn`s aren't first-class :/)
unsafe {
Self(fd::RawFileDescriptor::new_unchecked(fd))
}
}
#[inline]
pub fn take_ownership_of(fd: impl Into<fd::RawFileDescriptor>) -> Self
{
Self(fd.into())
}
#[inline]
pub fn take_ownership_of_raw(fd: impl Into<RawFd>) -> Result<Self, RawFd>
{
let fd = fd.into();
Ok(Self(fd.try_into().map_err(|_| fd)?))
}
#[inline]
pub unsafe fn take_ownership_of_raw_unchecked(fd: impl Into<RawFd>) -> Self
{
Self(fd::RawFileDescriptor::new_unchecked(fd.into()))
}
/// Attempt to link this instance's fd to another container over an fd
///
/// This is a safe wrapper around `dup2()`, as `clone()` is a safe wrapper around `dup()`.
///
/// # Note
/// If `T` is a buffered container (e.g. `std::fs::File`), make sure the buffer is flushed *before* calling this method on it, or the buffered data will be lost.
pub fn try_link<'o, T: ?Sized>(&self, other: &'o mut T) -> Result<&'o mut T, error::DuplicateError>
where T: AsRawFd
{
if unsafe {
libc::dup2(self.0.get(), other.as_raw_fd())
} < 0 {
Err(error::DuplicateError::new_dup2(self, other))
} else {
Ok(other)
}
}
/// Attempt to duplicate this raw file
pub fn try_clone(&self) -> Result<Self, error::DuplicateError>
{
match unsafe { libc::dup(self.0.get()) }
{
-1 => Err(error::DuplicateError::new_dup(self)),
fd => Ok(Self::take_ownership_of_unchecked(fd))
}
}
/// Consume a managed file into a raw file, attempting to synchronise it first.
///
/// # Note
/// This method attempts to sync the file's data.
/// To also attempt to sync the file's metadata, set `metadata` to true.
///
/// # Returns
/// If the sync should fail, the original file is returned, along with the error from the sync.
#[inline(always)]
pub fn try_from_file_synced(file: fs::File, metadata: bool) -> Result<Self, (fs::File, io::Error)>
{
match if metadata {
file.sync_all()
} else {
file.sync_data()
} {
Ok(()) => unsafe {
Ok(Self::from_raw_fd(file.into_raw_fd()))
},
Err(ioe) => Err((file, ioe))
}
}
/// Consume a managed fd type into a raw file
#[inline(always)]
pub fn from_file(file: impl IntoRawFd) -> Self
{
unsafe {
Self::from_raw_fd(file.into_raw_fd())
}
}
/// Consume into another managed file type container
#[inline(always)]
pub fn into_file<T: FromRawFd>(self) -> T
{
unsafe {
T::from_raw_fd(self.into_raw_fd())
}
}
/// Attempt to open a new raw file with these options
#[inline]
pub fn open(path: impl AsRef<Path>, opt: impl Borrow<fs::OpenOptions>) -> io::Result<Self>
{
opt.borrow().open(path).map(Into::into)
}
/// Open a new in-memory (W+R) file with an optional name and a fixed size.
#[cfg_attr(feature="logging", instrument(err))]
pub fn open_mem(name: Option<&str>, len: usize) -> Result<Self, error::MemfileError>
{
lazy_static! {
static ref DEFAULT_NAME: String = format!(concat!("<memfile@", file!(), "->", "{}", ":", line!(), "-", column!(), ">"), function!()); //TODO: If it turns out memfd_create() requires an `&'static str`; remove the use of stackalloc, and have this variable be a nul-terminated CString instead.
}
use libc::{
memfd_create,
fallocate,
};
use error::MemfileCreationStep::*;
let rname = name.unwrap_or(&DEFAULT_NAME);
stackalloc::alloca_zeroed(rname.len()+1, move |bname| { //XXX: Isn't the whole point of making `name` `&'static` that I don't know if `memfd_create()` requires static-lifetime name strings? TODO: Check this
#[cfg(feature="logging")]
let _span = info_span!("stack_name_cpy", size = bname.len());
#[cfg(feature="logging")]
let _span_lock = _span.enter();
macro_rules! attempt_call
{
($errcon:literal, $expr:expr, $step:expr) => {
//if_trace!(debug!("attempting systemcall"));
match unsafe {
$expr
} {
$errcon => {
if_trace!(warn!("systemcall failed: {}", error::raw_errno()));
Err($step)
},
x => Ok(x)
}
}
}
if_trace!(trace!("copying {rname:p} `{rname}' (sz: {}) -> nul-terminated {:p}", rname.len(), bname));
let bname = {
unsafe {
std::ptr::copy_nonoverlapping(rname.as_ptr(), bname.as_mut_ptr(), rname.len());
}
debug_assert_eq!(bname[rname.len()], 0, "Copied name string not null-terminated?");
bname.as_ptr()
};
let fd = attempt_call!(-1, memfd_create(bname as *const _, MEMFD_CREATE_FLAGS), Create(name.map(str::to_owned), MEMFD_CREATE_FLAGS))
.map(Self::take_ownership_of_unchecked)?; // Ensures `fd` is dropped if any subsequent calls fail
if len > 0 {
attempt_call!(-1
, fallocate(fd.0.get(), 0, 0, len.try_into()
.map_err(|_| Allocate(None, len))?)
, Allocate(Some(fd.fileno().clone()), len))?;
if cfg!(debug_assertions) {
if_trace!(trace!("Allocated {len} bytes to memory buffer"));
let seeked;
assert_eq!(attempt_call!(-1
, { seeked = libc::lseek(fd.0.get(), 0, libc::SEEK_CUR); seeked }
, io::Error::last_os_error())
.expect("Failed to check seek position in fd")
, 0, "memfd seek position is non-zero after fallocate()");
if_trace!(if seeked != 0 { warn!("Trace offset is non-zero: {seeked}") } else { trace!("Trace offset verified ok") });
}
} else {
if_trace!(trace!("No length provided, skipping fallocate() call"));
}
Ok(fd)
})
}
}
impl io::Write for RawFile
{
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match unsafe {
libc::write(self.0.get(), buf.as_ptr() as *const _, buf.len())
} {
-1 => Err(io::Error::last_os_error()),
wr => Ok(wr as usize)
}
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
// Not buffered
Ok(())
}
#[inline]
fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
// SAFETY: IoSlice is guaranteed to be ABI-compatible with `struct iovec`
match unsafe {
libc::writev(self.0.get(), bufs.as_ptr() as *const _, bufs.len() as i32)
} {
-1 => Err(io::Error::last_os_error()),
wr => Ok(wr as usize)
}
}
}
impl io::Read for RawFile
{
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match unsafe {
libc::read(self.0.get(), buf.as_mut_ptr() as *mut _, buf.len())
} {
-1 => Err(io::Error::last_os_error()),
wr => Ok(wr as usize)
}
}
#[inline]
fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
// SAFETY: IoSlice is guaranteed to be ABI-compatible with `struct iovec`
match unsafe {
libc::readv(self.0.get(), bufs.as_mut_ptr() as *mut _, bufs.len() as i32)
} {
-1 => Err(io::Error::last_os_error()),
wr => Ok(wr as usize)
}
}
}
impl From<fs::File> for RawFile
{
#[inline]
fn from(from: fs::File) -> Self
{
Self::from_file(from)
}
}
impl From<RawFile> for fs::File
{
#[inline]
fn from(from: RawFile) -> Self
{
from.into_file()
}
}
impl ops::Drop for RawFile
{
#[inline]
fn drop(&mut self) {
unsafe {
libc::close(self.0.get());
}
}
}
impl AsRawFd for RawFile
{
#[inline]
fn as_raw_fd(&self) -> RawFd {
self.0.get()
}
}
impl FromRawFd for RawFile
{
#[inline]
unsafe fn from_raw_fd(fd: RawFd) -> Self {
Self(fd::RawFileDescriptor::new(fd))
}
}
impl IntoRawFd for RawFile
{
#[inline]
fn into_raw_fd(self) -> RawFd {
let fd = self.0.get();
mem::forget(self); // prevent close
fd
}
}
#[cfg(test)]
mod tests
{
use super::*;
#[test]
fn memory_mapping() -> eyre::Result<()>
{
use std::io::*;
const STRING: &[u8] = b"Hello world!";
let mut file = {
let mut file = RawFile::open_mem(None, 4096)?;
file.write_all(STRING)?;
let mut file = fs::File::from(file);
file.seek(SeekFrom::Start(0))?;
file
};
let v: Vec<u8> = stackalloc::alloca_zeroed(STRING.len(), |buf| {
file.read_exact(buf).map(|_| buf.into())
})?;
assert_eq!(v.len(), STRING.len(), "Invalid read size.");
assert_eq!(&v[..], &STRING[..], "Invalid read data.");
Ok(())
}
}