1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
//! 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: 1.44 MB (floppy disk).
pub const MAX_PASTE_SIZE: usize = 1_440 * 1024;
/// Chunk size for large pastes: 8 KiB.
/// The DHT fragments datagrams into 896-byte pieces with a
/// maximum of 10 fragments (~8960 bytes reassembled). After
/// subtracting the Paste header (17 bytes) and StoreMsg overhead,
/// 8 KiB fits comfortably within one DHT message.
/// Pastes larger than this are split into chunks, each stored
/// as a separate DHT value, with a version-2 manifest that
/// lists the chunk hashes.
pub const CHUNK_SIZE: usize = 8 * 1024;
/// Current format version (single paste).
const FORMAT_VERSION: u8 = 1;
/// Format version for chunked paste manifests.
pub const FORMAT_VERSION_CHUNKED: u8 = 2;
/// 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.saturating_add(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());
}
}
|