//! Paste record format. //! //! Binary format (no external serialization deps): //! version: u8 //! created_at: u64 (big-endian) //! ttl_secs: u64 (big-endian) //! content: [u8] (remaining bytes) use tesseras_dht::sha2::{Digest, Sha256}; /// Maximum paste size: 64 KiB. pub const MAX_PASTE_SIZE: usize = 64 * 1024; /// Current format version. const FORMAT_VERSION: u8 = 1; /// Header size: version(1) + created_at(8) + ttl(8) = 17. const HEADER_SIZE: usize = 17; /// A paste record stored locally and replicated via the DHT. #[derive(Debug, Clone)] pub struct Paste { pub version: u8, pub content: Vec, pub created_at: u64, pub ttl_secs: u64, } impl Paste { /// Create a new paste with the current timestamp. pub fn new(content: Vec, ttl_secs: u64) -> Self { let created_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); Paste { version: FORMAT_VERSION, content, created_at, ttl_secs, } } /// Compute the SHA-256 content-addressed key (32 bytes). pub fn content_key(content: &[u8]) -> [u8; 32] { let mut h = Sha256::new(); h.update(content); h.finalize().into() } /// Serialize to bytes. pub fn to_bytes(&self) -> Vec { let mut buf = Vec::with_capacity(HEADER_SIZE + self.content.len()); buf.push(self.version); buf.extend_from_slice(&self.created_at.to_be_bytes()); buf.extend_from_slice(&self.ttl_secs.to_be_bytes()); buf.extend_from_slice(&self.content); buf } /// Deserialize from bytes. pub fn from_bytes(data: &[u8]) -> Option { if data.len() < HEADER_SIZE { return None; } let version = data[0]; let created_at = u64::from_be_bytes(data[1..9].try_into().ok()?); let ttl_secs = u64::from_be_bytes(data[9..17].try_into().ok()?); let content = data[HEADER_SIZE..].to_vec(); Some(Paste { version, content, created_at, ttl_secs, }) } /// Whether this paste has expired. pub fn is_expired(&self) -> bool { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); now > self.created_at + self.ttl_secs } } #[cfg(test)] mod tests { use super::*; #[test] fn round_trip() { let paste = Paste::new(b"hello world".to_vec(), 3600); let bytes = paste.to_bytes(); let decoded = Paste::from_bytes(&bytes).unwrap(); assert_eq!(decoded.content, b"hello world"); assert_eq!(decoded.ttl_secs, 3600); assert_eq!(decoded.version, FORMAT_VERSION); } #[test] fn content_key_deterministic() { let k1 = Paste::content_key(b"test"); let k2 = Paste::content_key(b"test"); assert_eq!(k1, k2); } #[test] fn content_key_differs() { let k1 = Paste::content_key(b"aaa"); let k2 = Paste::content_key(b"bbb"); assert_ne!(k1, k2); } #[test] fn too_short_fails() { assert!(Paste::from_bytes(&[0u8; 5]).is_none()); } #[test] fn empty_content() { let paste = Paste::new(Vec::new(), 60); let bytes = paste.to_bytes(); let decoded = Paste::from_bytes(&bytes).unwrap(); assert!(decoded.content.is_empty()); } }