aranya-runtime 0.24.0

The Aranya core runtime
Documentation
//! Spill backends for braid and convergence overflow data.
//!
//! Two independent backends, each behind its own feature flag and both
//! implementing [`Spill`]. When both features are enabled, both
//! types coexist and the caller picks which one to plug in.
//!
//! - [`LibcSpill`] (`libc`): file-backed via `aranya_libc` pread/pwrite,
//!   using the same APIs as `linear::libc`. The caller supplies a
//!   directory; the underlying file is created unlinked inside it and
//!   cleaned up when the handle is dropped.
//! - [`MemSpill`] (`testing`): in-memory `Vec<u8>` buffer,
//!   suitable for unit tests and environments without a filesystem.

#[cfg(feature = "testing")]
use alloc::vec::Vec;

use crate::{StorageError, storage::Spill};

// --- libc backend ---

/// File-backed spill. Created unlinked inside a caller-supplied directory
/// and cleaned up when the handle is dropped.
#[cfg(feature = "libc")]
pub struct LibcSpill {
    fd: aranya_libc::OwnedFd,
}

#[cfg(feature = "libc")]
impl LibcSpill {
    /// Create a new spill file inside `dir`. The file is immediately
    /// unlinked so it has no externally visible name and is cleaned up
    /// when this handle is dropped.
    pub fn new<P: AsRef<aranya_libc::Path>>(dir: P) -> Result<Self, StorageError> {
        use aranya_libc::{
            self as libc, O_CLOEXEC, O_CREAT, O_DIRECTORY, O_EXCL, O_RDONLY, O_RDWR, S_IRUSR,
            S_IWUSR,
        };

        static COUNTER: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
        let id = COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed);

        let dir_fd = libc::open(dir.as_ref(), O_RDONLY | O_DIRECTORY | O_CLOEXEC, 0)
            .map_err(|_| StorageError::IoError)?;

        let name = alloc::format!(".aranya_spill_{}\0", id);
        let file_path = aranya_libc::Path::new(name.as_bytes());

        let fd = libc::openat(
            libc::AsFd::as_fd(&dir_fd),
            file_path,
            O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC,
            S_IRUSR | S_IWUSR,
        )
        .map_err(|_| StorageError::IoError)?;

        // Unlink immediately — file stays open via fd, cleaned up on drop.
        let _ = libc::unlinkat(libc::AsFd::as_fd(&dir_fd), file_path, 0);

        Ok(Self { fd })
    }
}

#[cfg(feature = "libc")]
impl Spill for LibcSpill {
    fn write_at(&mut self, offset: usize, buf: &[u8]) -> Result<(), StorageError> {
        use aranya_libc::{self as libc, Errno};
        use buggy::BugExt as _;

        let mut off = i64::try_from(offset).assume("`offset` fits in i64")?;
        let mut remaining = buf;
        while !remaining.is_empty() {
            match libc::pwrite(&self.fd, remaining, off) {
                Ok(0) => return Err(StorageError::IoError),
                Ok(n) => {
                    remaining = remaining.get(n..).assume("`n` is in bounds")?;
                    off = off
                        .checked_add(i64::try_from(n).assume("write within bounds")?)
                        .assume("write within bounds")?;
                }
                Err(Errno::EINTR) => {}
                Err(_) => return Err(StorageError::IoError),
            }
        }
        Ok(())
    }

    fn read_at(&mut self, offset: usize, buf: &mut [u8]) -> Result<(), StorageError> {
        use aranya_libc::{self as libc, Errno};
        use buggy::BugExt as _;

        let mut off = i64::try_from(offset).assume("`offset` fits in i64")?;
        let mut remaining = buf;
        while !remaining.is_empty() {
            match libc::pread(&self.fd, remaining, off) {
                Ok(0) => return Err(StorageError::IoError),
                Ok(n) => {
                    remaining = remaining.get_mut(n..).assume("`n` is in bounds")?;
                    off = off
                        .checked_add(i64::try_from(n).assume("read within bounds")?)
                        .assume("read within bounds")?;
                }
                Err(Errno::EINTR) => {}
                Err(_) => return Err(StorageError::IoError),
            }
        }
        Ok(())
    }
}

// --- testing (in-memory) backend ---

/// In-memory spill backed by a growable byte buffer.
#[cfg(feature = "testing")]
pub struct MemSpill {
    buf: Vec<u8>,
}

#[cfg(feature = "testing")]
impl MemSpill {
    /// Create an empty in-memory spill.
    pub fn new() -> Result<Self, StorageError> {
        Ok(Self { buf: Vec::new() })
    }
}

#[cfg(feature = "testing")]
impl Spill for MemSpill {
    fn write_at(&mut self, offset: usize, data: &[u8]) -> Result<(), StorageError> {
        let end = offset
            .checked_add(data.len())
            .ok_or(StorageError::IoError)?;
        if end > self.buf.len() {
            self.buf.resize(end, 0);
        }
        self.buf[offset..end].copy_from_slice(data);
        Ok(())
    }

    fn read_at(&mut self, offset: usize, data: &mut [u8]) -> Result<(), StorageError> {
        let end = offset
            .checked_add(data.len())
            .ok_or(StorageError::IoError)?;
        let src = self.buf.get(offset..end).ok_or(StorageError::IoError)?;
        data.copy_from_slice(src);
        Ok(())
    }
}