verit-core 0.2.0

Internal: portable core engine for Exavian Veritate. Not a public API — depend on `verit`.
Documentation
//! `.vertc` — the Veritate **container**: an at-rest file holding many messages
//! for zero-copy random access, designed to be **`mmap`-ped and read in place**.
//!
//! A Veritate message is already zero-copy over a `&[u8]`, so a file of messages
//! needs only enough structure to (a) find each record without scanning and (b)
//! keep every record's internal 8-byte fields aligned once the file is mapped.
//! The container does exactly that and nothing more:
//!
//! # Superseded
//!
//! **This module is superseded by [`crate::file`] (the `.verit` file) and is
//! removed in 0.3.0.** See
//! [ADR-0002](https://github.com/exavianlabs/veritate/blob/main/docs/decisions/ADR-0002%20-%20The%20verit%20File%20Format.md).
//!
//! A `.vertc` container is a record array: it has no schema section, so it is
//! **not** interpretable without an out-of-band registry — the one property a
//! file format exists to provide. `.verit` adds that section (each schema stored
//! once, records hash-only), plus crash-safe appends, removal, and compaction.
//! Migrate with [`FileBuilder`](crate::FileBuilder).
//!
//! ```text
//! ┌─ header (32 B, 8-aligned) ─────────────────────────────────────────────┐
//! │ 0  "VRTC"        magic                                                   │
//! │ 4  u8  version=1                                                         │
//! │ 5  u8  flags=0                                                           │
//! │ 6  u16 reserved=0                                                        │
//! │ 8  u32 record_count                                                      │
//! │ 12 u32 reserved=0                                                        │
//! │ 16 u64 index_offset   (absolute offset of the index)                    │
//! │ 24 u64 file_len       (== total bytes; sanity)                          │
//! ├─ records ───────────────────────────────────────────────────────────────┤
//! │ each message's raw bytes, in order, every record 8-byte aligned          │
//! ├─ index (at index_offset, 8-aligned) ─────────────────────────────────────┤
//! │ record_count × { u64 offset, u64 len }   (offset absolute, len unpadded) │
//! └──────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! Because each record starts on an 8-byte boundary *within* the file and a
//! memory map begins on a page boundary (a multiple of 8), every record's start
//! address is 8-aligned in memory, so the message's own 8-aligned loads stay
//! aligned — the point of the layout. The reader borrows the mapped bytes:
//! [`Container::get`] returns a `&[u8]` sub-slice you hand straight to
//! [`Message::parse`]. No `mmap` dependency lives here (the library stays
//! zero-dep) — map the file with your platform's facility, or `std::fs::read`
//! it, and pass the `&[u8]`.

#![allow(deprecated)]

use crate::error::{Error, Result};

/// Container file magic: "VRTC" + this reader/writer implements version 1.
pub const CONTAINER_MAGIC: &[u8; 4] = b"VRTC";
/// Container format version this build reads and writes.
pub const CONTAINER_VERSION: u8 = 1;
/// Fixed header length, and the record alignment.
pub const CONTAINER_HEADER_LEN: usize = 32;
const ALIGN: usize = 8;
const INDEX_ENTRY_LEN: usize = 16;

#[inline]
fn align_up(x: usize, a: usize) -> usize {
    (x + a - 1) & !(a - 1)
}

/// Build a `.vertc` container from a sequence of Veritate messages. The output
/// is a self-contained `Vec<u8>` you write to a file (and later `mmap`).
///
/// ```
/// # #![allow(deprecated)]
/// # use verit_core::container::{ContainerWriter, Container};
/// let mut w = ContainerWriter::new();
/// w.add(b"\x00message-a");
/// w.add(b"\x01message-b");
/// let file = w.finish();
///
/// let c = Container::parse(&file).unwrap();
/// assert_eq!(c.len(), 2);
/// assert_eq!(c.get(1).unwrap(), b"\x01message-b");
/// ```
#[derive(Default)]
#[deprecated(
    since = "0.2.0",
    note = "superseded by the `.verit` file: use `FileBuilder` (in memory) or `FileWriter` (on disk). \
            A `.vertc` container carries no schema section, so it cannot be read without an \
            out-of-band registry. Removed in 0.3.0 — see ADR-0002."
)]
pub struct ContainerWriter {
    buf: Vec<u8>,
    // (absolute offset, unpadded length) per record.
    index: Vec<(u64, u64)>,
}

impl ContainerWriter {
    pub fn new() -> ContainerWriter {
        ContainerWriter {
            buf: vec![0; CONTAINER_HEADER_LEN], // header patched in `finish`
            index: Vec::new(),
        }
    }

    /// Append one message's bytes as the next record. Records are stored in the
    /// order added and read back by that index.
    pub fn add(&mut self, message: &[u8]) -> &mut Self {
        let pad = align_up(self.buf.len(), ALIGN) - self.buf.len();
        self.buf.resize(self.buf.len() + pad, 0);
        let offset = self.buf.len() as u64;
        self.buf.extend_from_slice(message);
        self.index.push((offset, message.len() as u64));
        self
    }

    /// Number of records added so far.
    pub fn len(&self) -> usize {
        self.index.len()
    }

    pub fn is_empty(&self) -> bool {
        self.index.is_empty()
    }

    /// Finish the container and return its bytes: pad to the index alignment,
    /// append the index, then patch the header.
    pub fn finish(mut self) -> Vec<u8> {
        let pad = align_up(self.buf.len(), ALIGN) - self.buf.len();
        self.buf.resize(self.buf.len() + pad, 0);
        let index_offset = self.buf.len() as u64;
        for (off, len) in &self.index {
            self.buf.extend_from_slice(&off.to_le_bytes());
            self.buf.extend_from_slice(&len.to_le_bytes());
        }
        let file_len = self.buf.len() as u64;

        self.buf[0..4].copy_from_slice(CONTAINER_MAGIC);
        self.buf[4] = CONTAINER_VERSION;
        // buf[5] flags, buf[6..8] reserved already zero.
        self.buf[8..12].copy_from_slice(&(self.index.len() as u32).to_le_bytes());
        // buf[12..16] reserved already zero.
        self.buf[16..24].copy_from_slice(&index_offset.to_le_bytes());
        self.buf[24..32].copy_from_slice(&file_len.to_le_bytes());
        self.buf
    }
}

/// A read-only view over a `.vertc` container's bytes (e.g. an `mmap`). Parsing
/// validates the header and the whole index up front, so every later
/// [`get`](Container::get) is a bounds-free slice.
#[derive(Clone, Debug)]
#[deprecated(
    since = "0.2.0",
    note = "superseded by the `.verit` file: use `FileView`. A `.vertc` container carries no \
            schema section, so it cannot be read without an out-of-band registry. \
            Removed in 0.3.0 — see ADR-0002."
)]
pub struct Container<'a> {
    buf: &'a [u8],
    index_offset: usize,
    count: usize,
}

impl<'a> Container<'a> {
    /// Validate the header and index of a container image. Rejects bad
    /// magic/version, an out-of-range or unaligned index, and any record whose
    /// extent falls outside the record region or is misaligned.
    pub fn parse(buf: &'a [u8]) -> Result<Container<'a>> {
        if buf.len() < CONTAINER_HEADER_LEN {
            return Err(Error::Truncated);
        }
        if &buf[0..4] != CONTAINER_MAGIC {
            return Err(Error::BadContainer("bad magic"));
        }
        if buf[4] != CONTAINER_VERSION {
            return Err(Error::BadContainer("unsupported container version"));
        }
        if buf[5] != 0 || u16::from_le_bytes(buf[6..8].try_into().unwrap()) != 0 {
            return Err(Error::BadContainer("nonzero reserved header field"));
        }
        let count = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize;
        let index_offset = u64::from_le_bytes(buf[16..24].try_into().unwrap());
        let file_len = u64::from_le_bytes(buf[24..32].try_into().unwrap());
        if file_len as usize != buf.len() {
            return Err(Error::BadContainer("file length mismatch"));
        }
        let index_offset = usize::try_from(index_offset)
            .map_err(|_| Error::BadContainer("index offset overflow"))?;
        if index_offset % ALIGN != 0 || index_offset < CONTAINER_HEADER_LEN {
            return Err(Error::BadContainer("misaligned index offset"));
        }
        // The index must fit exactly between its start and end of file.
        let index_bytes = count
            .checked_mul(INDEX_ENTRY_LEN)
            .ok_or(Error::BadContainer("index size overflow"))?;
        let index_end = index_offset
            .checked_add(index_bytes)
            .ok_or(Error::BadContainer("index end overflow"))?;
        if index_end > buf.len() {
            return Err(Error::BadContainer("index out of bounds"));
        }
        let container = Container {
            buf,
            index_offset,
            count,
        };
        // Validate every entry once so `get` is infallible on bounds.
        for i in 0..count {
            let (off, len) = container.raw_entry(i);
            let off =
                usize::try_from(off).map_err(|_| Error::BadContainer("record offset overflow"))?;
            let len =
                usize::try_from(len).map_err(|_| Error::BadContainer("record length overflow"))?;
            if off % ALIGN != 0 {
                return Err(Error::BadContainer("misaligned record"));
            }
            let end = off
                .checked_add(len)
                .ok_or(Error::BadContainer("record extent overflow"))?;
            // Records live strictly in [header, index_offset).
            if off < CONTAINER_HEADER_LEN || end > index_offset {
                return Err(Error::BadContainer("record outside record region"));
            }
        }
        Ok(container)
    }

    #[inline]
    fn raw_entry(&self, i: usize) -> (u64, u64) {
        let base = self.index_offset + i * INDEX_ENTRY_LEN;
        let off = u64::from_le_bytes(self.buf[base..base + 8].try_into().unwrap());
        let len = u64::from_le_bytes(self.buf[base + 8..base + 16].try_into().unwrap());
        (off, len)
    }

    /// Number of records.
    pub fn len(&self) -> usize {
        self.count
    }

    pub fn is_empty(&self) -> bool {
        self.count == 0
    }

    /// The raw message bytes of record `i`, borrowing the container image.
    /// Hand the result straight to [`Message::parse`](crate::Message::parse).
    pub fn get(&self, i: usize) -> Result<&'a [u8]> {
        if i >= self.count {
            return Err(Error::IndexOutOfBounds);
        }
        let (off, len) = self.raw_entry(i);
        // Validated in `parse`; this cannot go out of bounds.
        Ok(&self.buf[off as usize..(off + len) as usize])
    }

    /// Iterate over each record's bytes in order.
    pub fn iter(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
        (0..self.count).map(move |i| self.get(i).expect("index validated in parse"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn round_trips_messages() {
        let msgs: Vec<Vec<u8>> = vec![
            b"a".to_vec(),
            b"".to_vec(),
            (0..100u8).collect(),
            b"the last one".to_vec(),
        ];
        let mut w = ContainerWriter::new();
        for m in &msgs {
            w.add(m);
        }
        assert_eq!(w.len(), 4);
        let file = w.finish();

        let c = Container::parse(&file).unwrap();
        assert_eq!(c.len(), 4);
        for (i, m) in msgs.iter().enumerate() {
            assert_eq!(c.get(i).unwrap(), &m[..]);
        }
        let collected: Vec<&[u8]> = c.iter().collect();
        assert_eq!(collected.len(), 4);
        assert!(c.get(4).is_err());
    }

    #[test]
    fn records_are_eight_byte_aligned() {
        let mut w = ContainerWriter::new();
        w.add(b"odd-length-7").add(b"x"); // force padding between records
        let file = w.finish();
        let c = Container::parse(&file).unwrap();
        for i in 0..c.len() {
            let (off, _) = c.raw_entry(i);
            assert_eq!(off % 8, 0, "record {i} not 8-aligned");
        }
    }

    #[test]
    fn empty_container_is_valid() {
        let file = ContainerWriter::new().finish();
        let c = Container::parse(&file).unwrap();
        assert_eq!(c.len(), 0);
        assert!(c.is_empty());
    }

    #[test]
    fn rejects_corruption() {
        let mut file = {
            let mut w = ContainerWriter::new();
            w.add(b"hello");
            w.finish()
        };
        assert!(Container::parse(&file[..10]).is_err(), "truncated");

        let mut bad_magic = file.clone();
        bad_magic[0] = b'X';
        assert!(matches!(
            Container::parse(&bad_magic),
            Err(Error::BadContainer(_))
        ));

        let mut bad_ver = file.clone();
        bad_ver[4] = 2;
        assert!(matches!(
            Container::parse(&bad_ver),
            Err(Error::BadContainer(_))
        ));

        // Corrupt a record offset in the index to point past the record region.
        let idx_off = u64::from_le_bytes(file[16..24].try_into().unwrap()) as usize;
        file[idx_off..idx_off + 8].copy_from_slice(&u64::MAX.to_le_bytes());
        assert!(matches!(
            Container::parse(&file),
            Err(Error::BadContainer(_))
        ));
    }
}