#![allow(deprecated)]
use crate::error::{Error, Result};
pub const CONTAINER_MAGIC: &[u8; 4] = b"VRTC";
pub const CONTAINER_VERSION: u8 = 1;
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)
}
#[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>,
index: Vec<(u64, u64)>,
}
impl ContainerWriter {
pub fn new() -> ContainerWriter {
ContainerWriter {
buf: vec![0; CONTAINER_HEADER_LEN], index: Vec::new(),
}
}
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
}
pub fn len(&self) -> usize {
self.index.len()
}
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
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;
self.buf[8..12].copy_from_slice(&(self.index.len() as u32).to_le_bytes());
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
}
}
#[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> {
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"));
}
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,
};
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"))?;
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)
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn get(&self, i: usize) -> Result<&'a [u8]> {
if i >= self.count {
return Err(Error::IndexOutOfBounds);
}
let (off, len) = self.raw_entry(i);
Ok(&self.buf[off as usize..(off + len) as usize])
}
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"); 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(_))
));
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(_))
));
}
}