1use serde::{Deserialize, Serialize};
2use serde_bencode::ser;
3use serde_bytes::ByteBuf;
4use sha1::{Digest, Sha1};
5use tracing::error;
6use url::Url;
7
8use super::info_hash::InfoHash;
9use crate::utils::hex::{from_bytes, into_bytes};
10
11#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
12pub struct Torrent {
13 pub info: TorrentInfoDictionary, #[serde(default)]
15 pub announce: Option<String>,
16 #[serde(default)]
17 pub nodes: Option<Vec<(String, i64)>>,
18 #[serde(default)]
19 pub encoding: Option<String>,
20 #[serde(default)]
21 pub httpseeds: Option<Vec<String>>,
22 #[serde(default)]
23 #[serde(rename = "announce-list")]
24 pub announce_list: Option<Vec<Vec<String>>>,
25 #[serde(default)]
26 #[serde(rename = "creation date")]
27 pub creation_date: Option<i64>,
28 #[serde(default)]
29 pub comment: Option<String>,
30 #[serde(default)]
31 #[serde(rename = "created by")]
32 pub created_by: Option<String>,
33}
34
35#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
36pub struct TorrentInfoDictionary {
37 pub name: String,
38 #[serde(default)]
39 pub pieces: Option<ByteBuf>,
40 #[serde(rename = "piece length")]
41 pub piece_length: i64,
42 #[serde(default)]
43 pub md5sum: Option<String>,
44 #[serde(default)]
45 pub length: Option<i64>,
46 #[serde(default)]
47 pub files: Option<Vec<TorrentFile>>,
48 #[serde(default)]
49 pub private: Option<u8>,
50 #[serde(default)]
51 pub path: Option<Vec<String>>,
52 #[serde(default)]
53 #[serde(rename = "root hash")]
54 pub root_hash: Option<String>,
55 #[serde(default)]
56 pub source: Option<String>,
57}
58
59#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
60pub struct TorrentFile {
61 pub path: Vec<String>,
62 pub length: i64,
63 #[serde(default)]
64 pub md5sum: Option<String>,
65}
66
67impl Torrent {
68 #[must_use]
75 pub fn from_database(
76 db_torrent: &DbTorrent,
77 torrent_files: &[TorrentFile],
78 torrent_announce_urls: Vec<Vec<String>>,
79 torrent_http_seed_urls: Vec<String>,
80 torrent_nodes: Vec<(String, i64)>,
81 ) -> Self {
82 let pieces_or_root_hash = if db_torrent.is_bep_30 == 0 {
83 if let Some(pieces) = &db_torrent.pieces {
84 pieces.clone()
85 } else {
86 error!("Invalid torrent #{}. Null `pieces` in database", db_torrent.torrent_id);
87 String::new()
88 }
89 } else {
90 if let Some(root_hash) = &db_torrent.root_hash {
92 root_hash.clone()
93 } else {
94 error!("Invalid torrent #{}. Null `root_hash` in database", db_torrent.torrent_id);
95 String::new()
96 }
97 };
98
99 let info_dict = TorrentInfoDictionary::with(
100 &db_torrent.name,
101 db_torrent.piece_length,
102 db_torrent.private,
103 db_torrent.is_bep_30,
104 &pieces_or_root_hash,
105 torrent_files,
106 );
107
108 Self {
109 info: info_dict,
110 announce: None,
111 nodes: if torrent_nodes.is_empty() { None } else { Some(torrent_nodes) },
112 encoding: db_torrent.encoding.clone(),
113 httpseeds: if torrent_http_seed_urls.is_empty() {
114 None
115 } else {
116 Some(torrent_http_seed_urls)
117 },
118 announce_list: Some(torrent_announce_urls),
119 creation_date: db_torrent.creation_date,
120 comment: db_torrent.comment.clone(),
121 created_by: db_torrent.created_by.clone(),
122 }
123 }
124
125 pub fn include_url_as_main_tracker(&mut self, tracker_url: &Url) {
130 self.set_announce_to(tracker_url);
131 self.add_url_to_front_of_announce_list(tracker_url);
132 }
133
134 pub fn set_announce_to(&mut self, tracker_url: &Url) {
136 self.announce = Some(tracker_url.to_owned().to_string());
137 }
138
139 pub fn add_url_to_front_of_announce_list(&mut self, tracker_url: &Url) {
149 if let Some(list) = &mut self.announce_list {
150 for inner_list in list.iter_mut() {
152 inner_list.retain(|url| *url != tracker_url.to_string());
153 }
154
155 let vec = vec![tracker_url.to_owned().to_string()];
157 list.insert(0, vec);
158
159 list.retain(|inner_list| !inner_list.is_empty());
161 }
162 }
163
164 pub fn reset_announce_list_if_private(&mut self) {
166 if self.is_private() {
167 self.announce_list = None;
168 }
169 }
170
171 fn is_private(&self) -> bool {
172 if let Some(private) = self.info.private {
173 if private == 1 {
174 return true;
175 }
176 }
177 false
178 }
179
180 #[must_use]
186 pub fn calculate_info_hash_as_bytes(&self) -> [u8; 20] {
187 let info_bencoded = ser::to_bytes(&self.info).expect("variable `info` was not able to be serialized.");
188 let mut hasher = Sha1::new();
189 hasher.update(info_bencoded);
190 let sum_hex = hasher.finalize();
191 let mut sum_bytes: [u8; 20] = Default::default();
192 sum_bytes.copy_from_slice(sum_hex.as_slice());
193 sum_bytes
194 }
195
196 #[must_use]
197 pub fn canonical_info_hash(&self) -> InfoHash {
198 self.calculate_info_hash_as_bytes().into()
199 }
200
201 #[must_use]
202 pub fn canonical_info_hash_hex(&self) -> String {
203 self.canonical_info_hash().to_hex_string()
204 }
205
206 #[must_use]
207 pub fn file_size(&self) -> i64 {
208 match self.info.length {
209 Some(length) => length,
210 None => match &self.info.files {
211 None => 0,
212 Some(files) => {
213 let mut file_size = 0;
214 for file in files {
215 file_size += file.length;
216 }
217 file_size
218 }
219 },
220 }
221 }
222
223 #[must_use]
229 pub fn announce_urls(&self) -> Vec<String> {
230 match &self.announce_list {
231 Some(list) => list.clone().into_iter().flatten().collect::<Vec<String>>(),
232 None => vec![self.announce.clone().expect("variable `announce` should not be None")],
233 }
234 }
235
236 #[must_use]
237 pub fn is_a_single_file_torrent(&self) -> bool {
238 self.info.is_a_single_file_torrent()
239 }
240
241 #[must_use]
242 pub fn is_a_multiple_file_torrent(&self) -> bool {
243 self.info.is_a_multiple_file_torrent()
244 }
245}
246
247impl TorrentInfoDictionary {
248 #[must_use]
257 pub fn with(
258 name: &str,
259 piece_length: i64,
260 private: Option<u8>,
261 is_bep_30: i64,
262 pieces_or_root_hash: &str,
263 files: &[TorrentFile],
264 ) -> Self {
265 let mut info_dict = Self {
266 name: name.to_string(),
267 pieces: None,
268 piece_length,
269 md5sum: None,
270 length: None,
271 files: None,
272 private,
273 path: None,
274 root_hash: None,
275 source: None,
276 };
277
278 if is_bep_30 == 0 {
281 let buffer = into_bytes(pieces_or_root_hash).expect("variable `torrent_info.pieces` is not a valid hex string");
282 info_dict.pieces = Some(ByteBuf::from(buffer));
283 } else {
284 info_dict.root_hash = Some(pieces_or_root_hash.to_owned());
285 }
286
287 if files.len() == 1 {
289 let torrent_file = files
290 .first()
291 .expect("vector `torrent_files` should have at least one element");
292
293 info_dict.md5sum.clone_from(&torrent_file.md5sum); info_dict.length = Some(torrent_file.length);
296
297 let path = if torrent_file
298 .path
299 .first()
300 .as_ref()
301 .expect("the vector for the `path` should have at least one element")
302 .is_empty()
303 {
304 None
305 } else {
306 Some(torrent_file.path.clone())
307 };
308
309 info_dict.path = path;
310 } else {
311 info_dict.files = Some(files.to_vec());
312 }
313
314 info_dict
315 }
316
317 #[must_use]
320 pub fn get_pieces_as_string(&self) -> String {
321 match &self.pieces {
322 None => String::new(),
323 Some(byte_buf) => from_bytes(byte_buf.as_ref()),
324 }
325 }
326
327 #[must_use]
330 pub fn get_root_hash_as_string(&self) -> String {
331 match &self.root_hash {
332 None => String::new(),
333 Some(root_hash) => root_hash.clone(),
334 }
335 }
336
337 #[must_use]
339 pub fn is_bep_30(&self) -> bool {
340 self.root_hash.is_some()
341 }
342
343 #[must_use]
344 pub fn is_a_single_file_torrent(&self) -> bool {
345 self.length.is_some()
346 }
347
348 #[must_use]
349 pub fn is_a_multiple_file_torrent(&self) -> bool {
350 self.files.is_some()
351 }
352}
353
354#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
355pub struct DbTorrent {
356 pub torrent_id: i64,
357 pub info_hash: String,
358 pub name: String,
359 pub pieces: Option<String>,
360 pub root_hash: Option<String>,
361 pub piece_length: i64,
362 #[serde(default)]
363 pub private: Option<u8>,
364 pub is_bep_30: i64,
365 pub comment: Option<String>,
366 pub creation_date: Option<i64>,
367 pub created_by: Option<String>,
368 pub encoding: Option<String>,
369}
370
371#[allow(clippy::module_name_repetitions)]
372#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
373pub struct DbTorrentFile {
374 pub path: Option<String>,
375 pub length: i64,
376 #[serde(default)]
377 pub md5sum: Option<String>,
378}
379
380#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
381pub struct DbTorrentAnnounceUrl {
382 pub tracker_url: String,
383}
384
385#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
386pub struct DbTorrentHttpSeedUrl {
387 pub seed_url: String,
388}
389
390#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
391pub struct DbTorrentNode {
392 pub node_ip: String,
393 pub node_port: i64,
394}
395
396#[cfg(test)]
397mod tests {
398
399 mod info_hash_calculation_for_version_v1 {
400
401 use serde_bytes::ByteBuf;
402
403 use crate::models::torrent_file::{Torrent, TorrentInfoDictionary};
404
405 #[test]
406 fn the_parsed_torrent_file_should_calculated_the_torrent_info_hash() {
407 let sample_data_in_txt_file = "mandelbrot\n";
439
440 let info = TorrentInfoDictionary {
441 name: "sample.txt".to_string(),
442 pieces: Some(ByteBuf::from(vec![
443 212, 145, 88, 127, 28, 66, 223, 240, 203, 15, 245, 194, 184, 206, 254, 34, 179, 173, 49, 10, ])),
446 piece_length: 16384,
447 md5sum: None,
448 length: Some(sample_data_in_txt_file.len().try_into().unwrap()),
449 files: None,
450 private: None,
451 path: None,
452 root_hash: None,
453 source: None,
454 };
455
456 let torrent = Torrent {
457 info: info.clone(),
458 announce: None,
459 announce_list: Some(vec![]),
460 creation_date: None,
461 comment: None,
462 created_by: None,
463 nodes: None,
464 encoding: None,
465 httpseeds: None,
466 };
467
468 assert_eq!(torrent.canonical_info_hash_hex(), "79fa9e4a2927804fe4feab488a76c8c2d3d1cdca");
469 }
470
471 mod infohash_should_be_calculated_for {
472
473 use serde_bytes::ByteBuf;
474
475 use crate::models::torrent_file::{Torrent, TorrentFile, TorrentInfoDictionary};
476
477 #[test]
478 fn a_simple_single_file_torrent() {
479 let sample_data_in_txt_file = "mandelbrot\n";
480
481 let info = TorrentInfoDictionary {
482 name: "sample.txt".to_string(),
483 pieces: Some(ByteBuf::from(vec![
484 212, 145, 88, 127, 28, 66, 223, 240, 203, 15, 245, 194, 184, 206, 254, 34, 179, 173, 49, 10, ])),
487 piece_length: 16384,
488 md5sum: None,
489 length: Some(sample_data_in_txt_file.len().try_into().unwrap()),
490 files: None,
491 private: None,
492 path: None,
493 root_hash: None,
494 source: None,
495 };
496
497 let torrent = Torrent {
498 info: info.clone(),
499 announce: None,
500 announce_list: Some(vec![]),
501 creation_date: None,
502 comment: None,
503 created_by: None,
504 nodes: None,
505 encoding: None,
506 httpseeds: None,
507 };
508
509 assert_eq!(torrent.canonical_info_hash_hex(), "79fa9e4a2927804fe4feab488a76c8c2d3d1cdca");
510 }
511
512 #[test]
513 fn a_simple_multi_file_torrent() {
514 let sample_data_in_txt_file = "mandelbrot\n";
515
516 let info = TorrentInfoDictionary {
517 name: "sample".to_string(),
518 pieces: Some(ByteBuf::from(vec![
519 212, 145, 88, 127, 28, 66, 223, 240, 203, 15, 245, 194, 184, 206, 254, 34, 179, 173, 49, 10, ])),
522 piece_length: 16384,
523 md5sum: None,
524 length: None,
525 files: Some(vec![TorrentFile {
526 path: vec!["sample.txt".to_string()],
527 length: sample_data_in_txt_file.len().try_into().unwrap(),
528 md5sum: None,
529 }]),
530 private: None,
531 path: None,
532 root_hash: None,
533 source: None,
534 };
535
536 let torrent = Torrent {
537 info: info.clone(),
538 announce: None,
539 announce_list: Some(vec![]),
540 creation_date: None,
541 comment: None,
542 created_by: None,
543 nodes: None,
544 encoding: None,
545 httpseeds: None,
546 };
547
548 assert_eq!(torrent.canonical_info_hash_hex(), "aa2aca91ab650c4d249c475ca3fa604f2ccb0d2a");
549 }
550
551 #[test]
552 fn a_simple_single_file_torrent_with_a_source() {
553 let sample_data_in_txt_file = "mandelbrot\n";
554
555 let info = TorrentInfoDictionary {
556 name: "sample.txt".to_string(),
557 pieces: Some(ByteBuf::from(vec![
558 212, 145, 88, 127, 28, 66, 223, 240, 203, 15, 245, 194, 184, 206, 254, 34, 179, 173, 49, 10, ])),
561 piece_length: 16384,
562 md5sum: None,
563 length: Some(sample_data_in_txt_file.len().try_into().unwrap()),
564 files: None,
565 private: None,
566 path: None,
567 root_hash: None,
568 source: Some("ABC".to_string()), };
570
571 let torrent = Torrent {
572 info: info.clone(),
573 announce: None,
574 announce_list: Some(vec![]),
575 creation_date: None,
576 comment: None,
577 created_by: None,
578 nodes: None,
579 encoding: None,
580 httpseeds: None,
581 };
582
583 assert_eq!(torrent.canonical_info_hash_hex(), "ccc1cf4feb59f3fa85c96c9be1ebbafcfe8a9cc8");
584 }
585
586 #[test]
587 fn a_simple_single_file_private_torrent() {
588 let sample_data_in_txt_file = "mandelbrot\n";
589
590 let info = TorrentInfoDictionary {
591 name: "sample.txt".to_string(),
592 pieces: Some(ByteBuf::from(vec![
593 212, 145, 88, 127, 28, 66, 223, 240, 203, 15, 245, 194, 184, 206, 254, 34, 179, 173, 49, 10, ])),
596 piece_length: 16384,
597 md5sum: None,
598 length: Some(sample_data_in_txt_file.len().try_into().unwrap()),
599 files: None,
600 private: Some(1),
601 path: None,
602 root_hash: None,
603 source: None,
604 };
605
606 let torrent = Torrent {
607 info: info.clone(),
608 announce: None,
609 announce_list: Some(vec![]),
610 creation_date: None,
611 comment: None,
612 created_by: None,
613 nodes: None,
614 encoding: None,
615 httpseeds: None,
616 };
617
618 assert_eq!(torrent.canonical_info_hash_hex(), "d3a558d0a19aaa23ba6f9f430f40924d10fefa86");
619 }
620 }
621 }
622}