use crate::error::{Result, TensogramError};
pub const MAGIC: &[u8; 8] = b"TENSOGRM";
pub const END_MAGIC: &[u8; 8] = b"39277777";
pub const FRAME_MAGIC: &[u8; 2] = b"FR";
pub const FRAME_END: &[u8; 4] = b"ENDF";
pub const WIRE_VERSION: u16 = 3;
pub const PREAMBLE_SIZE: usize = 24;
pub const FRAME_HEADER_SIZE: usize = 16;
pub const POSTAMBLE_SIZE: usize = 24;
pub const FRAME_COMMON_FOOTER_SIZE: usize = 12;
pub const DATA_OBJECT_FOOTER_SIZE: usize = 20;
#[inline]
pub fn footer_size_for(frame_type: FrameType) -> usize {
match frame_type {
FrameType::NTensorFrame => DATA_OBJECT_FOOTER_SIZE,
_ => FRAME_COMMON_FOOTER_SIZE,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum FrameType {
HeaderMetadata = 1,
HeaderIndex = 2,
HeaderHash = 3,
FooterHash = 5,
FooterIndex = 6,
FooterMetadata = 7,
PrecederMetadata = 8,
NTensorFrame = 9,
}
impl FrameType {
pub fn from_u16(v: u16) -> Result<Self> {
match v {
1 => Ok(FrameType::HeaderMetadata),
2 => Ok(FrameType::HeaderIndex),
3 => Ok(FrameType::HeaderHash),
4 => Err(TensogramError::Framing(
"reserved frame type 4 (obsolete v2 NTensorFrame) not supported in v3".to_string(),
)),
5 => Ok(FrameType::FooterHash),
6 => Ok(FrameType::FooterIndex),
7 => Ok(FrameType::FooterMetadata),
8 => Ok(FrameType::PrecederMetadata),
9 => Ok(FrameType::NTensorFrame),
_ => Err(TensogramError::Framing(format!("unknown frame type: {v}"))),
}
}
pub fn is_data_object(self) -> bool {
matches!(self, FrameType::NTensorFrame)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MessageFlags(u16);
impl MessageFlags {
pub const HEADER_METADATA: u16 = 1 << 0;
pub const FOOTER_METADATA: u16 = 1 << 1;
pub const HEADER_INDEX: u16 = 1 << 2;
pub const FOOTER_INDEX: u16 = 1 << 3;
pub const HEADER_HASHES: u16 = 1 << 4;
pub const FOOTER_HASHES: u16 = 1 << 5;
pub const PRECEDER_METADATA: u16 = 1 << 6;
pub const HASHES_PRESENT: u16 = 1 << 7;
pub fn new(bits: u16) -> Self {
Self(bits)
}
pub fn bits(self) -> u16 {
self.0
}
pub fn has(self, flag: u16) -> bool {
self.0 & flag != 0
}
pub fn set(&mut self, flag: u16) {
self.0 |= flag;
}
pub fn has_metadata(self) -> bool {
self.has(Self::HEADER_METADATA) || self.has(Self::FOOTER_METADATA)
}
}
pub struct DataObjectFlags;
impl DataObjectFlags {
pub const CBOR_AFTER_PAYLOAD: u16 = 1 << 0;
}
#[derive(Debug, Clone)]
pub struct Preamble {
pub version: u16,
pub flags: MessageFlags,
pub reserved: u32,
pub total_length: u64,
}
impl Preamble {
pub fn read_from(buf: &[u8]) -> Result<Self> {
if buf.len() < PREAMBLE_SIZE {
return Err(TensogramError::Framing(format!(
"buffer too short for preamble: {} < {PREAMBLE_SIZE}",
buf.len()
)));
}
if &buf[0..8] != MAGIC {
let actual = &buf[0..8];
let as_ascii: String = actual
.iter()
.map(|&b| if b.is_ascii_graphic() { b as char } else { '.' })
.collect();
let as_hex: String = actual.iter().map(|b| format!("{b:02x}")).collect();
return Err(TensogramError::Framing(format!(
"invalid magic bytes: expected \"TENSOGRM\", got \"{as_ascii}\" \
(hex {as_hex}) — buffer does not start with a Tensogram preamble"
)));
}
let version = read_u16_be(buf, 8);
if version != WIRE_VERSION {
return Err(TensogramError::Framing(format!(
"unsupported message version {version} (required = {WIRE_VERSION}); \
v3 is a clean break from v2 with no backward compatibility — \
re-encode with tensogram ≥ 0.17.0"
)));
}
Ok(Preamble {
version,
flags: MessageFlags::new(read_u16_be(buf, 10)),
reserved: read_u32_be(buf, 12),
total_length: read_u64_be(buf, 16),
})
}
pub fn write_to(&self, out: &mut Vec<u8>) {
out.extend_from_slice(MAGIC);
out.extend_from_slice(&self.version.to_be_bytes());
out.extend_from_slice(&self.flags.bits().to_be_bytes());
out.extend_from_slice(&self.reserved.to_be_bytes());
out.extend_from_slice(&self.total_length.to_be_bytes());
}
}
#[derive(Debug, Clone)]
pub struct FrameHeader {
pub frame_type: FrameType,
pub version: u16,
pub flags: u16,
pub total_length: u64,
}
impl FrameHeader {
pub fn read_from(buf: &[u8]) -> Result<Self> {
if buf.len() < FRAME_HEADER_SIZE {
return Err(TensogramError::Framing(format!(
"buffer too short for frame header: {} < {FRAME_HEADER_SIZE}",
buf.len()
)));
}
if &buf[0..2] != FRAME_MAGIC {
return Err(TensogramError::Framing(format!(
"invalid frame magic: {:?}",
&buf[0..2]
)));
}
let type_val = read_u16_be(buf, 2);
let frame_type = FrameType::from_u16(type_val)?;
Ok(FrameHeader {
frame_type,
version: read_u16_be(buf, 4),
flags: read_u16_be(buf, 6),
total_length: read_u64_be(buf, 8),
})
}
pub fn write_to(&self, out: &mut Vec<u8>) {
out.extend_from_slice(FRAME_MAGIC);
out.extend_from_slice(&(self.frame_type as u16).to_be_bytes());
out.extend_from_slice(&self.version.to_be_bytes());
out.extend_from_slice(&self.flags.to_be_bytes());
out.extend_from_slice(&self.total_length.to_be_bytes());
}
}
#[derive(Debug, Clone)]
pub struct Postamble {
pub first_footer_offset: u64,
pub total_length: u64,
}
impl Postamble {
pub fn read_from(buf: &[u8]) -> Result<Self> {
if buf.len() < POSTAMBLE_SIZE {
return Err(TensogramError::Framing(format!(
"buffer too short for postamble: {} < {POSTAMBLE_SIZE}",
buf.len()
)));
}
let first_footer_offset = read_u64_be(buf, 0);
let total_length = read_u64_be(buf, 8);
if &buf[16..24] != END_MAGIC {
return Err(TensogramError::Framing(
"invalid end magic in postamble".to_string(),
));
}
Ok(Postamble {
first_footer_offset,
total_length,
})
}
pub fn write_to(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.first_footer_offset.to_be_bytes());
out.extend_from_slice(&self.total_length.to_be_bytes());
out.extend_from_slice(END_MAGIC);
}
}
pub(crate) fn read_u16_be(buf: &[u8], offset: usize) -> u16 {
let mut bytes = [0u8; 2];
bytes.copy_from_slice(&buf[offset..offset + 2]);
u16::from_be_bytes(bytes)
}
pub(crate) fn read_u32_be(buf: &[u8], offset: usize) -> u32 {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&buf[offset..offset + 4]);
u32::from_be_bytes(bytes)
}
pub(crate) fn read_u64_be(buf: &[u8], offset: usize) -> u64 {
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&buf[offset..offset + 8]);
u64::from_be_bytes(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preamble_round_trip() {
let preamble = Preamble {
version: WIRE_VERSION,
flags: MessageFlags::new(MessageFlags::HEADER_METADATA | MessageFlags::HEADER_INDEX),
reserved: 0,
total_length: 4096,
};
let mut buf = Vec::new();
preamble.write_to(&mut buf);
assert_eq!(buf.len(), PREAMBLE_SIZE);
let parsed = Preamble::read_from(&buf).unwrap();
assert_eq!(parsed.version, WIRE_VERSION);
assert!(parsed.flags.has(MessageFlags::HEADER_METADATA));
assert!(parsed.flags.has(MessageFlags::HEADER_INDEX));
assert!(!parsed.flags.has(MessageFlags::FOOTER_INDEX));
assert_eq!(parsed.total_length, 4096);
}
#[test]
fn test_frame_header_round_trip() {
let fh = FrameHeader {
frame_type: FrameType::NTensorFrame,
version: 1,
flags: DataObjectFlags::CBOR_AFTER_PAYLOAD,
total_length: 1024,
};
let mut buf = Vec::new();
fh.write_to(&mut buf);
assert_eq!(buf.len(), FRAME_HEADER_SIZE);
let parsed = FrameHeader::read_from(&buf).unwrap();
assert_eq!(parsed.frame_type, FrameType::NTensorFrame);
assert_eq!(parsed.version, 1);
assert_eq!(parsed.flags, DataObjectFlags::CBOR_AFTER_PAYLOAD);
assert_eq!(parsed.total_length, 1024);
}
#[test]
fn test_postamble_round_trip() {
let pa = Postamble {
first_footer_offset: 8192,
total_length: 16384,
};
let mut buf = Vec::new();
pa.write_to(&mut buf);
assert_eq!(buf.len(), POSTAMBLE_SIZE);
let parsed = Postamble::read_from(&buf).unwrap();
assert_eq!(parsed.first_footer_offset, 8192);
assert_eq!(parsed.total_length, 16384);
}
#[test]
fn test_postamble_zero_total_length_streaming() {
let pa = Postamble {
first_footer_offset: 500,
total_length: 0,
};
let mut buf = Vec::new();
pa.write_to(&mut buf);
assert_eq!(buf.len(), POSTAMBLE_SIZE);
let parsed = Postamble::read_from(&buf).unwrap();
assert_eq!(parsed.total_length, 0);
}
#[test]
fn test_postamble_end_magic_at_fixed_offset() {
let pa = Postamble {
first_footer_offset: 1,
total_length: 2,
};
let mut buf = Vec::new();
pa.write_to(&mut buf);
assert_eq!(&buf[16..24], END_MAGIC);
}
#[test]
fn test_invalid_magic() {
let buf = vec![0u8; PREAMBLE_SIZE];
assert!(Preamble::read_from(&buf).is_err());
}
#[test]
fn test_invalid_magic_error_message_shows_actual_bytes() {
let mut buf = vec![0u8; PREAMBLE_SIZE];
buf[0..8].copy_from_slice(b"GARBAGE!");
let err = Preamble::read_from(&buf).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("TENSOGRM"), "expected magic mentioned: {msg}");
assert!(msg.contains("GARBAGE!"), "actual ASCII rendered: {msg}");
assert!(msg.contains("hex"), "hex representation shown: {msg}");
}
#[test]
fn test_v2_preamble_is_rejected() {
let mut buf = Vec::with_capacity(PREAMBLE_SIZE);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&2u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&64u64.to_be_bytes()); let err = Preamble::read_from(&buf).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("unsupported message version 2"),
"expected v2-rejection message, got: {msg}"
);
assert!(
msg.contains("required = 3"),
"expected required-version banner, got: {msg}"
);
}
#[test]
fn test_v1_preamble_is_rejected() {
let mut buf = Vec::with_capacity(PREAMBLE_SIZE);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&1u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&64u64.to_be_bytes());
let err = Preamble::read_from(&buf).unwrap_err();
assert!(err.to_string().contains("unsupported message version 1"));
}
#[test]
fn test_future_version_is_rejected() {
let mut buf = Vec::with_capacity(PREAMBLE_SIZE);
buf.extend_from_slice(MAGIC);
buf.extend_from_slice(&99u16.to_be_bytes());
buf.extend_from_slice(&0u16.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&64u64.to_be_bytes());
assert!(Preamble::read_from(&buf).is_err());
}
#[test]
fn test_invalid_frame_magic() {
let buf = vec![0u8; FRAME_HEADER_SIZE];
assert!(FrameHeader::read_from(&buf).is_err());
}
#[test]
fn test_invalid_end_magic() {
let mut buf = vec![0u8; POSTAMBLE_SIZE];
buf[0..8].copy_from_slice(&100u64.to_be_bytes());
buf[8..16].copy_from_slice(&200u64.to_be_bytes());
assert!(Postamble::read_from(&buf).is_err());
}
#[test]
fn test_frame_type_parse() {
assert_eq!(FrameType::from_u16(1).unwrap(), FrameType::HeaderMetadata);
assert_eq!(FrameType::from_u16(2).unwrap(), FrameType::HeaderIndex);
assert_eq!(FrameType::from_u16(3).unwrap(), FrameType::HeaderHash);
assert_eq!(FrameType::from_u16(5).unwrap(), FrameType::FooterHash);
assert_eq!(FrameType::from_u16(6).unwrap(), FrameType::FooterIndex);
assert_eq!(FrameType::from_u16(7).unwrap(), FrameType::FooterMetadata);
assert_eq!(FrameType::from_u16(8).unwrap(), FrameType::PrecederMetadata);
assert_eq!(FrameType::from_u16(9).unwrap(), FrameType::NTensorFrame);
assert!(FrameType::from_u16(0).is_err());
assert!(FrameType::from_u16(10).is_err());
}
#[test]
fn test_type_4_reserved_is_rejected() {
let err = FrameType::from_u16(4).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("reserved frame type 4"),
"expected reserved-type-4 message, got: {msg}"
);
assert!(
msg.contains("obsolete v2"),
"expected 'obsolete v2' in the error, got: {msg}"
);
}
#[test]
fn test_is_data_object() {
assert!(FrameType::NTensorFrame.is_data_object());
assert!(!FrameType::HeaderMetadata.is_data_object());
assert!(!FrameType::PrecederMetadata.is_data_object());
assert!(!FrameType::FooterHash.is_data_object());
assert!(!FrameType::FooterIndex.is_data_object());
assert!(!FrameType::FooterMetadata.is_data_object());
}
#[test]
fn test_message_flags() {
let mut flags = MessageFlags::default();
assert!(!flags.has_metadata());
flags.set(MessageFlags::HEADER_METADATA);
assert!(flags.has_metadata());
assert!(flags.has(MessageFlags::HEADER_METADATA));
assert!(!flags.has(MessageFlags::FOOTER_METADATA));
flags.set(MessageFlags::FOOTER_INDEX);
assert!(flags.has(MessageFlags::FOOTER_INDEX));
}
#[test]
fn test_preceder_metadata_flag() {
let mut flags = MessageFlags::default();
assert!(!flags.has(MessageFlags::PRECEDER_METADATA));
flags.set(MessageFlags::PRECEDER_METADATA);
assert!(flags.has(MessageFlags::PRECEDER_METADATA));
assert_eq!(flags.bits() & (1 << 6), 1 << 6);
}
#[test]
fn test_preceder_metadata_frame_header_round_trip() {
let fh = FrameHeader {
frame_type: FrameType::PrecederMetadata,
version: 1,
flags: 0,
total_length: 256,
};
let mut buf = Vec::new();
fh.write_to(&mut buf);
assert_eq!(buf.len(), FRAME_HEADER_SIZE);
let parsed = FrameHeader::read_from(&buf).unwrap();
assert_eq!(parsed.frame_type, FrameType::PrecederMetadata);
assert_eq!(parsed.version, 1);
assert_eq!(parsed.flags, 0);
assert_eq!(parsed.total_length, 256);
}
#[test]
fn test_truncated_preamble() {
let buf = vec![0u8; 10];
assert!(Preamble::read_from(&buf).is_err());
}
}