baryl 0.0.2

Public SDK for Baryl, a full-system emulation and introspection engine
//! Strings stored inline, in the two shapes they arrive in.
//!
//! A string that has to survive a checkpoint or cross to another binary cannot
//! be a `String` — a pointer into a host process that has exited means nothing.
//! It is stored as bytes in place instead, NUL-terminated, capped at a length
//! the layout fixes.
//!
//! [`FlatCStr<N>`](FlatCStr) is that as a type, with constructors and
//! comparison. The four free functions below are the same bytes where a C
//! header chose the layout, so the field arrives as a bare `[c_char; N]` with
//! no type on it.

use std::{
    ffi::{CStr, c_char},
    fmt,
    hash::{Hash, Hasher},
};

#[cfg(feature = "config")]
use serde::{Deserialize, Deserializer, de::Error};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};

use crate::abi::SandboxSafe;

/// Bytes an inline path field holds, terminator included. A path that does not
/// fit is refused, never truncated.
pub const MAX_PATH_LEN: usize = 256;

/// Bytes `[baryl] engine` holds, terminator included.
pub const MAX_ENGINE_NAME_LEN: usize = 256;
/// Bytes `[network] subnet` holds, terminator included.
pub const MAX_SUBNET_LEN: usize = 64;
/// Bytes one `KEY=VALUE` entry holds, terminator included.
pub const MAX_ENV_ENTRY_LEN: usize = 256;
/// How many `[env]` entries a machine config may carry.
pub const MAX_ENV_ENTRIES: usize = 32;

// FlatCStr

/// Up to `N - 1` bytes of UTF-8 and a NUL, stored in place as C's `char[N]`.
///
/// `N` counts the terminator, so a `FlatCStr<16>` holds a 15-byte name. `Copy`,
/// with no allocation and no lifetime, so one can sit in a `#[repr(C)]` struct
/// that goes into a checkpoint or across to another binary.
///
/// Equality and hashing compare the string, not the trailing bytes, so two
/// values that differ only past their NUL are equal.
///
/// # Examples
///
/// ```ignore
/// let name: FlatCStr<16> = FlatCStr::new("sshd");
/// assert_eq!(name.as_str(), "sshd");
/// assert_eq!(name.len(), 4);
/// assert_eq!(FlatCStr::<16>::capacity(), 16);
///
/// // 15 bytes fit; 16 do not, because the terminator needs one.
/// assert!(FlatCStr::<16>::try_new("123456789012345").is_some());
/// assert!(FlatCStr::<16>::try_new("1234567890123456").is_none());
/// ```
#[repr(C)]
#[derive(Clone, Copy, FromBytes, Immutable, KnownLayout)]
pub struct FlatCStr<const N: usize> {
    bytes: [u8; N],
}

// SAFETY: #[repr(C)] over a sole `[u8; N]`: alignment 1, size N, no padding.
unsafe impl<const N: usize> IntoBytes for FlatCStr<N> {
    fn only_derive_is_allowed_to_implement_this_trait() {}
}
unsafe impl<const N: usize> SandboxSafe for FlatCStr<N> {}

impl<const N: usize> FlatCStr<N> {
    /// `N`, the terminator's byte included.
    pub const fn capacity() -> usize {
        N
    }

    /// Store `s`, or `None` if it does not fit alongside its terminator. Use
    /// this for anything that came from a config file, a guest, or a user.
    ///
    /// # Panics
    ///
    /// If `s` holds a NUL byte. A string with one cannot be stored
    /// NUL-terminated at all, and it is a bug rather than bad input — this
    /// panics rather than answering `None`, so a length problem and an
    /// impossible string are not confused.
    pub fn try_new(s: &str) -> Option<Self> {
        assert!(!s.as_bytes().contains(&0), "FlatCStr: interior NUL in {s:?}");
        if s.len() + 1 > N {
            return None;
        }
        let mut bytes = [0u8; N];
        bytes[..s.len()].copy_from_slice(s.as_bytes());
        Some(Self { bytes: bytes })
    }

    /// Store `s`, for a literal you already know fits.
    ///
    /// # Panics
    ///
    /// If `s` does not fit, or holds a NUL byte. Reach for
    /// [`try_new`](Self::try_new) for anything whose length you did not write
    /// out yourself.
    pub fn new(s: &str) -> Self {
        Self::try_new(s).unwrap_or_else(|| panic!("FlatCStr: {} bytes exceeds {N}", s.len()))
    }

    /// All zero bytes: the empty string, and a valid default.
    pub fn empty() -> Self {
        Self { bytes: [0u8; N] }
    }

    /// A `const char *` to the bytes in place, for a call that takes one.
    /// Borrows `self`, so it lives exactly as long as this value does.
    pub fn as_ptr(&self) -> *const c_char {
        self.bytes.as_ptr().cast()
    }

    /// The bytes up to the terminator.
    ///
    /// Answers `""` when they are not valid UTF-8 — a field filled from guest
    /// memory can hold anything, and this never fails or panics on it.
    pub fn as_str(&self) -> &str {
        std::str::from_utf8(&self.bytes[..self.len()]).unwrap_or("")
    }

    /// Bytes before the terminator, or `N` if there is no terminator at all.
    pub fn len(&self) -> usize {
        self.bytes.iter().position(|&b| b == 0).unwrap_or(N)
    }

    /// True when the first byte is the terminator.
    pub fn is_empty(&self) -> bool {
        self.bytes[0] == 0
    }
}

impl<const N: usize> Default for FlatCStr<N> {
    fn default() -> Self {
        Self::empty()
    }
}

impl<const N: usize> PartialEq for FlatCStr<N> {
    fn eq(&self, other: &Self) -> bool {
        self.as_str() == other.as_str()
    }
}

impl<const N: usize> Eq for FlatCStr<N> {}

impl<const N: usize> Hash for FlatCStr<N> {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.as_str().hash(state);
    }
}

impl<const N: usize> fmt::Debug for FlatCStr<N> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_tuple("FlatCStr").field(&self.as_str()).finish()
    }
}

/// Reads a TOML string into the field's own bytes. A string over `N - 1` bytes,
/// or one holding a NUL, is a deserialize error naming the offending value.
#[cfg(feature = "config")]
impl<'de, const N: usize> Deserialize<'de> for FlatCStr<N> {
    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        let s = String::deserialize(d)?;
        if s.as_bytes().contains(&0) {
            return Err(D::Error::custom(format!("string holds a NUL byte: {s:?}")));
        }
        FlatCStr::try_new(&s)
            .ok_or_else(|| D::Error::custom(format!("{} bytes exceeds {N}: {s:?}", s.len())))
    }
}

// Inline name fields -- the conversions a bare `[c_char; N]` asks for

/// The field's bytes as `u8`, borrowed in place. All `N` of them, terminator
/// and trailing bytes included.
pub fn inline_bytes<const N: usize>(field: &[c_char; N]) -> &[u8] {
    // SAFETY: `c_char` and `u8` are one byte with the same validity, and the
    // borrow is `field`'s own.
    unsafe { std::slice::from_raw_parts(field.as_ptr().cast(), N) }
}

/// The same bytes, writable — what a guest read or a `copy_from_slice` fills.
pub fn inline_bytes_mut<const N: usize>(field: &mut [c_char; N]) -> &mut [u8] {
    // SAFETY: as `inline_bytes`.
    unsafe { std::slice::from_raw_parts_mut(field.as_mut_ptr().cast(), N) }
}

/// The field read as a string, up to its terminator.
///
/// A field with no terminator anywhere in it answers `c""` rather than running
/// off the end, so this is safe on bytes that came from the guest.
///
/// # Examples
///
/// ```ignore
/// // `Process::name` is one of these fields.
/// let proc = enl.process_by_name("sshd")?;
/// let name: &CStr = inline_cstr(&proc.name);
/// ```
pub fn inline_cstr<const N: usize>(field: &[c_char; N]) -> &CStr {
    CStr::from_bytes_until_nul(inline_bytes(field)).unwrap_or(c"")
}

/// Pack `src` into an inline field, keeping at most `N - 1` bytes so the
/// terminator always fits.
///
/// **Truncates.** A name longer than the field is cut, not refused — the field
/// has no way to report an error, so the short answer is the answer. Where you
/// need to know, check `src.to_bytes().len() < N` first.
///
/// `N` must be at least 1; `pack_cstr::<0>` fails to compile.
pub fn pack_cstr<const N: usize>(src: &CStr) -> [c_char; N] {
    const { assert!(N > 0, "an inline name field has room for its terminator") };
    let mut out = [0 as c_char; N];
    let bytes = src.to_bytes();
    let n = bytes.len().min(N - 1);
    inline_bytes_mut(&mut out)[..n].copy_from_slice(&bytes[..n]);
    out
}