//! Bundles: how objects travel between a workspace and a host. //! //! ```text //! "VCSB1\n" //! frame*: id[32] kind[1] codec[1] raw_len[u64 le] payload_len[u64 le] payload //! end: 32 zero bytes //! ``` //! //! The codec says how the payload is stored (0 raw, 1 zstd); hosts compress //! and decompress. A receiver checks every object against its ID. use crate::object::{Kind, Object}; use crate::{Id, Result, corrupt}; pub const MAGIC: &[u8] = b"VCSB1\n"; pub const RAW: u8 = 0; pub const ZSTD: u8 = 1; /// No single object may claim to be bigger than this. pub const MAX_OBJECT: u64 = 256 << 20; /// One stored object as it travels: possibly compressed. #[derive(Debug, Clone)] pub struct Frame { pub id: Id, pub kind: Kind, pub codec: u8, pub raw_len: u64, pub payload: Vec, } impl Frame { pub fn raw(object: &Object) -> Frame { Frame { id: object.id(), kind: object.kind, codec: RAW, raw_len: object.data.len() as u64, payload: object.data.clone() } } } pub fn write_header(out: &mut Vec) { out.extend_from_slice(MAGIC); } pub fn write_frame(out: &mut Vec, f: &Frame) { out.extend_from_slice(&f.id.0); out.push(f.kind as u8); out.push(f.codec); out.extend_from_slice(&f.raw_len.to_le_bytes()); out.extend_from_slice(&(f.payload.len() as u64).to_le_bytes()); out.extend_from_slice(&f.payload); } pub fn write_end(out: &mut Vec) { out.extend_from_slice(&[0u8; 32]); } /// Reads frames from a complete bundle. pub fn read(bytes: &[u8]) -> Result> { if !bytes.starts_with(MAGIC) { return Err(corrupt("not an Onset bundle")); } let mut at = MAGIC.len(); let mut out = Vec::new(); loop { let id_bytes = bytes.get(at..at + 32).ok_or_else(|| corrupt("bundle ends early"))?; if id_bytes.iter().all(|b| *b == 0) { break; } let id = Id::from_slice(id_bytes).unwrap(); let header = bytes.get(at + 32..at + 50).ok_or_else(|| corrupt("bundle ends early"))?; let kind = Kind::from_u8(header[0]).ok_or_else(|| corrupt("bad object kind in bundle"))?; let codec = header[1]; if codec != RAW && codec != ZSTD { return Err(corrupt("unknown codec in bundle")); } let raw_len = u64::from_le_bytes(header[2..10].try_into().unwrap()); let payload_len = u64::from_le_bytes(header[10..18].try_into().unwrap()); if raw_len > MAX_OBJECT || payload_len > MAX_OBJECT { return Err(crate::Error::Limit("an object in the bundle is too big".into())); } let start = at + 50; let end = start + payload_len as usize; let payload = bytes.get(start..end).ok_or_else(|| corrupt("bundle ends early"))?.to_vec(); if codec == RAW && payload_len != raw_len { return Err(corrupt("raw payload length mismatch")); } out.push(Frame { id, kind, codec, raw_len, payload }); at = end; } Ok(out) } /// Checks a decoded object against the ID its frame claimed. pub fn verify(frame: &Frame, data: &[u8]) -> Result<()> { if data.len() as u64 != frame.raw_len { return Err(corrupt(format!("object {} has the wrong length", frame.id.short()))); } if Id::of(frame.kind as u8, data) != frame.id { return Err(corrupt(format!("object {} doesn't match its ID", frame.id.short()))); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn round_trip() { let a = Object::blob(b"hello".to_vec()); let b = Object::new(Kind::Tree, Vec::new()); let mut out = Vec::new(); write_header(&mut out); write_frame(&mut out, &Frame::raw(&a)); write_frame(&mut out, &Frame::raw(&b)); write_end(&mut out); let frames = read(&out).unwrap(); assert_eq!(frames.len(), 2); verify(&frames[0], &frames[0].payload).unwrap(); assert!(verify(&frames[0], b"other").is_err()); assert_eq!(frames[1].kind, Kind::Tree); } }