aboutsummaryrefslogtreecommitdiffstats
path: root/src/paste.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-25 02:07:37 -0300
committermurilo ijanc2026-03-25 02:07:37 -0300
commit7aff2e1d279a4e442b32f49ca0a0eca065355787 (patch)
treebc987ece7eb78bb8375de1b20123ecd0f90472ba /src/paste.rs
downloadtesseras-paste-7aff2e1d279a4e442b32f49ca0a0eca065355787.tar.gz
Initial commit: tesseras-paste decentralized pastebin
DHT-backed encrypted pastebin with two binaries (tp/tpd), XChaCha20-Poly1305 encryption, content-addressed storage, and Unix socket + HTTP interfaces.
Diffstat (limited to 'src/paste.rs')
-rw-r--r--src/paste.rs128
1 files changed, 128 insertions, 0 deletions
diff --git a/src/paste.rs b/src/paste.rs
new file mode 100644
index 0000000..50b32b1
--- /dev/null
+++ b/src/paste.rs
@@ -0,0 +1,128 @@
+//! 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<u8>,
+ pub created_at: u64,
+ pub ttl_secs: u64,
+}
+
+impl Paste {
+ /// Create a new paste with the current timestamp.
+ pub fn new(content: Vec<u8>, 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<u8> {
+ 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<Self> {
+ 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());
+ }
+}