use crate::error::{Error, Result};
use crate::integrity::crc32c;
use crate::limits::Limits;
pub const RECORD_HEADER_LEN: usize = 8;
pub const RECORD_TRAILER_LEN: usize = 4;
pub const RECORD_OVERHEAD: usize = RECORD_HEADER_LEN + RECORD_TRAILER_LEN;
pub const FLAG_OPTIONAL: u8 = 0x01;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum RecordTag {
Universe = 0x01,
Format = 0x02,
Object = 0x10,
Graph = 0x20,
Model = 0x30,
EntropyChannel = 0x40,
Residual = 0x50,
Checkpoint = 0x60,
Index = 0x70,
ExternalRef = 0x80,
Integrity = 0xF0,
Trailer = 0xFF,
}
impl RecordTag {
pub const fn from_u8(b: u8) -> Option<RecordTag> {
match b {
0x01 => Some(RecordTag::Universe),
0x02 => Some(RecordTag::Format),
0x10 => Some(RecordTag::Object),
0x20 => Some(RecordTag::Graph),
0x30 => Some(RecordTag::Model),
0x40 => Some(RecordTag::EntropyChannel),
0x50 => Some(RecordTag::Residual),
0x60 => Some(RecordTag::Checkpoint),
0x70 => Some(RecordTag::Index),
0x80 => Some(RecordTag::ExternalRef),
0xF0 => Some(RecordTag::Integrity),
0xFF => Some(RecordTag::Trailer),
_ => None,
}
}
pub const fn name(self) -> &'static str {
match self {
RecordTag::Universe => "UNIVERSE",
RecordTag::Format => "FORMAT",
RecordTag::Object => "OBJECT",
RecordTag::Graph => "GRAPH",
RecordTag::Model => "MODEL",
RecordTag::EntropyChannel => "ENTROPY_CHANNEL",
RecordTag::Residual => "RESIDUAL",
RecordTag::Checkpoint => "CHECKPOINT",
RecordTag::Index => "INDEX",
RecordTag::ExternalRef => "EXTERNAL_REF",
RecordTag::Integrity => "INTEGRITY",
RecordTag::Trailer => "TRAILER",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
pub tag: u8,
pub flags: u8,
pub payload: Vec<u8>,
}
impl Record {
pub fn new(tag: u8, payload: Vec<u8>) -> Self {
Record {
tag,
flags: 0,
payload,
}
}
pub fn known(tag: RecordTag, payload: Vec<u8>) -> Self {
Record {
tag: tag as u8,
flags: 0,
payload,
}
}
pub fn is_optional(&self) -> bool {
self.flags & FLAG_OPTIONAL != 0
}
}
pub fn write_record(out: &mut Vec<u8>, tag: u8, flags: u8, payload: &[u8]) -> Result<()> {
let len = u32::try_from(payload.len())
.map_err(|_| Error::resource_limit("record payload exceeds 4 GiB"))?;
let start = out.len();
out.push(tag);
out.push(flags);
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(payload);
let crc = crc32c(&out[start..]);
out.extend_from_slice(&crc.to_le_bytes());
Ok(())
}
pub struct RecordReader<'a> {
data: &'a [u8],
pos: usize,
limits: Limits,
count: u32,
}
impl<'a> RecordReader<'a> {
pub fn new(data: &'a [u8], offset: usize, limits: Limits) -> Self {
RecordReader {
data,
pos: offset,
limits,
count: 0,
}
}
pub fn position(&self) -> usize {
self.pos
}
pub fn records_read(&self) -> u32 {
self.count
}
pub fn next_record(&mut self) -> Result<Option<Record>> {
if self.pos == self.data.len() {
return Ok(None);
}
if self.count >= self.limits.max_record_count {
return Err(Error::resource_limit("record count limit exceeded"));
}
let remaining = self.data.len() - self.pos;
if remaining < RECORD_OVERHEAD {
return Err(Error::invalid_container(format!(
"truncated record header: {remaining} bytes remain"
)));
}
let hdr = &self.data[self.pos..self.pos + RECORD_HEADER_LEN];
let tag = hdr[0];
let flags = hdr[1];
let reserved = u16::from_le_bytes([hdr[2], hdr[3]]);
if reserved != 0 {
return Err(Error::invalid_container(
"record reserved field must be zero",
));
}
let len = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
if len > self.limits.max_record_len {
return Err(Error::resource_limit(format!(
"record payload length {len} exceeds limit {}",
self.limits.max_record_len
)));
}
let total = RECORD_HEADER_LEN
.checked_add(len as usize)
.and_then(|n| n.checked_add(RECORD_TRAILER_LEN))
.ok_or_else(|| Error::invalid_container("record length overflow"))?;
if total > remaining {
return Err(Error::invalid_container(format!(
"truncated record payload: need {total}, have {remaining}"
)));
}
let body = &self.data[self.pos..self.pos + RECORD_HEADER_LEN + len as usize];
let want_crc = u32::from_le_bytes([
self.data[self.pos + RECORD_HEADER_LEN + len as usize],
self.data[self.pos + RECORD_HEADER_LEN + len as usize + 1],
self.data[self.pos + RECORD_HEADER_LEN + len as usize + 2],
self.data[self.pos + RECORD_HEADER_LEN + len as usize + 3],
]);
let got_crc = crc32c(body);
if want_crc != got_crc {
return Err(Error::invalid_container(format!(
"record CRC32C mismatch at offset {}: declared {want_crc:#010x}, computed {got_crc:#010x}",
self.pos
)));
}
let payload = self.data
[self.pos + RECORD_HEADER_LEN..self.pos + RECORD_HEADER_LEN + len as usize]
.to_vec();
self.pos += total;
self.count += 1;
Ok(Some(Record {
tag,
flags,
payload,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_read_roundtrip() {
let mut buf = Vec::new();
write_record(&mut buf, RecordTag::Universe as u8, 0, b"hello").unwrap();
write_record(&mut buf, RecordTag::Object as u8, 0, &[1, 2, 3]).unwrap();
let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
let a = r.next_record().unwrap().unwrap();
assert_eq!(a.tag, RecordTag::Universe as u8);
assert_eq!(a.payload, b"hello");
let b = r.next_record().unwrap().unwrap();
assert_eq!(b.tag, RecordTag::Object as u8);
assert_eq!(b.payload, vec![1, 2, 3]);
assert!(r.next_record().unwrap().is_none());
}
#[test]
fn detects_payload_corruption() {
let mut buf = Vec::new();
write_record(&mut buf, 0x10, 0, b"abcdef").unwrap();
buf[9] ^= 0xFF; let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
let e = r.next_record().unwrap_err();
assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
}
#[test]
fn detects_truncation() {
let mut buf = Vec::new();
write_record(&mut buf, 0x10, 0, b"abcdef").unwrap();
buf.truncate(buf.len() - 2);
let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
let e = r.next_record().unwrap_err();
assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
}
#[test]
fn rejects_nonzero_reserved() {
let mut buf = Vec::new();
write_record(&mut buf, 0x10, 0, b"x").unwrap();
buf[2] = 1; let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
let e = r.next_record().unwrap_err();
assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
}
#[test]
fn enforces_record_length_limit() {
let mut buf = Vec::new();
write_record(&mut buf, 0x10, 0, &[0u8; 100]).unwrap();
let limits = Limits {
max_record_len: 10,
..Limits::DEFAULT
};
let mut r = RecordReader::new(&buf, 0, limits);
let e = r.next_record().unwrap_err();
assert_eq!(e.class(), crate::ErrorClass::ResourceLimit);
}
}