1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5pub struct DhtNodeEntry {
6 pub host: String,
8 pub port: i64,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct PeerStrikeEntry {
15 pub ip: String,
17 pub count: i64,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct SessionState {
29 #[serde(rename = "dht-nodes", default)]
31 pub dht_nodes: Vec<DhtNodeEntry>,
32 #[serde(
35 rename = "dht-node-id",
36 default,
37 skip_serializing_if = "Option::is_none"
38 )]
39 pub dht_node_id: Option<String>,
40 #[serde(rename = "torrents", default)]
42 pub torrents: Vec<irontide_core::FastResumeData>,
43 #[serde(rename = "banned-peers", default)]
45 pub banned_peers: Vec<String>,
46 #[serde(rename = "peer-strikes", default)]
48 pub peer_strikes: Vec<PeerStrikeEntry>,
49}
50
51impl SessionState {
52 #[must_use]
54 pub fn new() -> Self {
55 Self {
56 dht_nodes: Vec::new(),
57 dht_node_id: None,
58 torrents: Vec::new(),
59 banned_peers: Vec::new(),
60 peer_strikes: Vec::new(),
61 }
62 }
63}
64
65impl Default for SessionState {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71#[must_use]
77pub fn validate_resume_bitfield(pieces: &[u8], num_pieces: u32) -> bool {
78 if num_pieces == 0 {
79 return pieces.is_empty();
80 }
81 let expected = num_pieces.div_ceil(8) as usize;
82 pieces.len() == expected
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use pretty_assertions::assert_eq;
89
90 #[test]
91 fn session_state_bencode_round_trip() {
92 let state = SessionState {
93 dht_nodes: vec![
94 DhtNodeEntry {
95 host: "router.bittorrent.com".into(),
96 port: 6881,
97 },
98 DhtNodeEntry {
99 host: "dht.transmissionbt.com".into(),
100 port: 6881,
101 },
102 ],
103 dht_node_id: None,
104 torrents: vec![irontide_core::FastResumeData::new(
105 vec![0xAA; 20],
106 "test-torrent".into(),
107 "/downloads".into(),
108 )],
109 banned_peers: Vec::new(),
110 peer_strikes: Vec::new(),
111 };
112
113 let encoded = irontide_bencode::to_bytes(&state).unwrap();
114 let decoded: SessionState = irontide_bencode::from_bytes(&encoded).unwrap();
115 assert_eq!(state, decoded);
116 }
117
118 #[test]
119 fn session_state_with_node_id_round_trip() {
120 let state = SessionState {
121 dht_nodes: vec![DhtNodeEntry {
122 host: "1.2.3.4".into(),
123 port: 6881,
124 }],
125 dht_node_id: Some("26d8457c04424098fd9e615b297745c772f49706".into()),
126 torrents: vec![],
127 banned_peers: vec![],
128 peer_strikes: vec![],
129 };
130
131 let encoded = irontide_bencode::to_bytes(&state).unwrap();
132 let encoded_str = String::from_utf8_lossy(&encoded);
133 assert!(
134 encoded_str.contains("dht-node-id"),
135 "encoded bencode should contain dht-node-id key: {encoded_str}"
136 );
137
138 let decoded: SessionState = irontide_bencode::from_bytes(&encoded).unwrap();
139 assert_eq!(state.dht_node_id, decoded.dht_node_id);
140 }
141
142 #[test]
143 fn empty_session_state_round_trip() {
144 let state = SessionState::new();
145
146 let encoded = irontide_bencode::to_bytes(&state).unwrap();
147 let decoded: SessionState = irontide_bencode::from_bytes(&encoded).unwrap();
148 assert_eq!(state, decoded);
149 }
150
151 #[test]
152 fn validate_resume_bitfield_correct_length() {
153 assert!(validate_resume_bitfield(&[0xFF], 8));
155 assert!(validate_resume_bitfield(&[0xFF, 0x80], 9));
157 assert!(validate_resume_bitfield(&[0xFF, 0xFF], 16));
159 assert!(validate_resume_bitfield(&[0x80], 1));
161 }
162
163 #[test]
164 fn validate_resume_bitfield_wrong_length() {
165 assert!(!validate_resume_bitfield(&[0xFF, 0x00], 8));
167 assert!(!validate_resume_bitfield(&[0xFF], 9));
169 assert!(!validate_resume_bitfield(&[0x00], 0));
171 }
172
173 #[test]
174 fn validate_resume_bitfield_zero_pieces() {
175 assert!(validate_resume_bitfield(&[], 0));
177 }
178
179 #[test]
180 fn session_state_with_bans_round_trip() {
181 let state = SessionState {
182 dht_nodes: vec![],
183 dht_node_id: None,
184 torrents: vec![],
185 banned_peers: vec!["10.0.0.1".into(), "192.168.1.5".into()],
186 peer_strikes: vec![
187 PeerStrikeEntry {
188 ip: "10.0.0.1".into(),
189 count: 3,
190 },
191 PeerStrikeEntry {
192 ip: "10.0.0.2".into(),
193 count: 1,
194 },
195 ],
196 };
197
198 let encoded = irontide_bencode::to_bytes(&state).unwrap();
199 let decoded: SessionState = irontide_bencode::from_bytes(&encoded).unwrap();
200 assert_eq!(state, decoded);
201 assert_eq!(decoded.banned_peers.len(), 2);
202 assert_eq!(decoded.peer_strikes.len(), 2);
203 }
204
205 #[test]
206 fn session_state_backward_compatible() {
207 let old_state = SessionState {
209 dht_nodes: vec![DhtNodeEntry {
210 host: "example.com".into(),
211 port: 6881,
212 }],
213 dht_node_id: None,
214 torrents: vec![],
215 banned_peers: vec![],
216 peer_strikes: vec![],
217 };
218 let encoded = irontide_bencode::to_bytes(&old_state).unwrap();
219
220 let decoded: SessionState = irontide_bencode::from_bytes(&encoded).unwrap();
223 assert!(decoded.banned_peers.is_empty());
224 assert!(decoded.peer_strikes.is_empty());
225 assert_eq!(decoded.dht_nodes.len(), 1);
226 }
227}