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
//! Session persistence with the ecosystem codecs: binary via `rustbinary`,
//! human via `nextjson`, wall-clock via `tzcraft::Ticks`. The host stores
//! the produced bytes (file, KV store, mobile keychain).
use crate::error::{Error, Result};
use alloc::string::String;
use alloc::vec::Vec;
use nextjson::{NsonDeserialize, NsonSerialize};
/// Persisted state of one torrent.
#[derive(Debug, Clone, PartialEq, Eq, NsonSerialize, NsonDeserialize)]
pub struct TorrentState {
/// Info hash (20 or 32 bytes).
pub info_hash: Vec<u8>,
/// Save path.
pub save_path: String,
/// Verified piece bitfield (network bytes).
pub have: Vec<u8>,
/// Partially received blocks: (piece, block bitfield).
pub partial: Vec<(u32, Vec<u8>)>,
/// When added (unix epoch seconds, from the `tzcraft` timeline).
pub added_at: i64,
/// Paused flag.
pub paused: bool,
/// Per-file priority bytes (0=Skip, 1=Normal, 2=High); empty = all Normal.
pub file_priorities: Vec<u8>,
/// Per-task upload limit (bytes/s; 0 = unlimited).
pub upload_limit_bps: u64,
/// Per-task download limit (bytes/s; 0 = unlimited).
pub download_limit_bps: u64,
/// Anti-leech reputation ledger (`leech::ReputationStore`, opaque bytes).
pub reputation: Vec<u8>,
/// Raw bencoded `info` dict of the torrent (empty for magnets that have
/// not fetched metadata yet). Persisted so a magnet never re-fetches
/// metadata after a restart.
#[njson(default)]
pub info_raw: Vec<u8>,
}
/// Whole-session state.
#[derive(Debug, Clone, Default, PartialEq, Eq, NsonSerialize, NsonDeserialize)]
pub struct SessionState {
/// Format version.
pub version: u32,
/// Torrents.
pub torrents: Vec<TorrentState>,
/// DHT nodes to re-bootstrap from (compact 26-byte entries).
pub dht_nodes: Vec<Vec<u8>>,
}
impl SessionState {
/// Binary encode (rustbinary).
pub fn to_binary(&self) -> Result<Vec<u8>> {
let config = rustbinary::options().with_limit(64 * 1024 * 1024);
config.serialize(self).map_err(|_| Error::InvalidInput)
}
/// Binary decode (rustbinary).
pub fn from_binary(bytes: &[u8]) -> Result<SessionState> {
let config = rustbinary::options().with_limit(64 * 1024 * 1024);
config.deserialize(bytes).map_err(|_| Error::InvalidInput)
}
/// JSON encode (nextjson).
pub fn to_json(&self) -> Result<Vec<u8>> {
nextjson::nextencode(self).map_err(|_| Error::InvalidInput)
}
/// JSON decode (nextjson).
pub fn from_json(bytes: &[u8]) -> Result<SessionState> {
nextjson::nextdecode(bytes).map_err(|_| Error::InvalidInput)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> SessionState {
SessionState {
version: 1,
torrents: vec![TorrentState {
info_hash: vec![1u8; 20],
save_path: String::from("/data/downloads"),
have: vec![0b1010_0000, 0],
partial: vec![(3, vec![0b1000_0000])],
added_at: 1_700_000_000,
paused: false,
file_priorities: vec![1, 0, 2],
upload_limit_bps: 0,
download_limit_bps: 1_000_000,
reputation: vec![1, 2, 3],
info_raw: vec![4, 5, 6],
}],
dht_nodes: vec![vec![2u8; 26]],
}
}
#[test]
fn binary_roundtrip() {
let s = sample();
let bytes = s.to_binary().unwrap();
let back = SessionState::from_binary(&bytes).unwrap();
assert_eq!(back, s);
}
#[test]
fn json_roundtrip() {
let s = sample();
let bytes = s.to_json().unwrap();
let back = SessionState::from_json(&bytes).unwrap();
assert_eq!(back, s);
}
}