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
|
//! 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.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());
}
}
|