|
1 | 1 | use bt_bencode::Value as BencodeValue; |
| 2 | +use fluent_uri::pct_enc::{ |
| 3 | + encoder::{Data, Query}, |
| 4 | + EStr, EString, |
| 5 | +}; |
2 | 6 | use rustc_hex::ToHex; |
3 | 7 | use serde::{Deserialize, Serialize}; |
4 | 8 | use sha1::{Digest, Sha1}; |
5 | 9 |
|
6 | 10 | use std::collections::HashMap; |
7 | 11 | use std::path::PathBuf; |
8 | 12 |
|
9 | | -use crate::{InfoHash, InfoHashError, PieceLength, TorrentContent, TorrentID, Tracker}; |
| 13 | +use crate::{ |
| 14 | + InfoHash, InfoHashError, MagnetLink, MagnetLinkError, PieceLength, TorrentContent, TorrentID, |
| 15 | + Tracker, |
| 16 | +}; |
10 | 17 |
|
11 | 18 | /// Error occurred during parsing a [`TorrentFile`](crate::torrent_file::TorrentFile). |
12 | 19 | #[derive(Clone, Debug, PartialEq)] |
@@ -328,6 +335,70 @@ impl TorrentFile { |
328 | 335 | pub fn id(&self) -> TorrentID { |
329 | 336 | TorrentID::from_infohash(&self.hash) |
330 | 337 | } |
| 338 | + |
| 339 | + /// List the trackers in the torrent, ensuring |
| 340 | + /// they're sorted and deduplicated. |
| 341 | + pub fn trackers(&self) -> Vec<Tracker> { |
| 342 | + let mut trackers = vec![]; |
| 343 | + if let Some(tracker) = &self.decoded.announce { |
| 344 | + trackers.push(tracker.clone()); |
| 345 | + } |
| 346 | + |
| 347 | + for tier in &self.decoded.announce_list { |
| 348 | + for tracker in tier { |
| 349 | + trackers.push(tracker.clone()); |
| 350 | + } |
| 351 | + } |
| 352 | + |
| 353 | + trackers.sort_unstable(); |
| 354 | + trackers.dedup(); |
| 355 | + |
| 356 | + trackers |
| 357 | + } |
| 358 | + |
| 359 | + /// Lossy transformation into a [`MagnetLink`]. |
| 360 | + /// |
| 361 | + /// This is a lossy operation because: |
| 362 | + /// |
| 363 | + /// - magnet links do not contain detailed pieces information, |
| 364 | + /// so there is no reverse operation without network resolution |
| 365 | + /// - not all metadata about the torrent may be added to the magnet link |
| 366 | + /// |
| 367 | + /// What's added to the magnet link: |
| 368 | + /// |
| 369 | + /// - name |
| 370 | + /// - torrentID |
| 371 | + /// - trackers |
| 372 | + /// |
| 373 | + /// For the moment, this is a fallible operation because we make sure |
| 374 | + /// the produced MagnetLink can be parsed again. This operation may not |
| 375 | + /// produce errors in a future release. |
| 376 | + pub fn magnet_link(&self) -> Result<MagnetLink, MagnetLinkError> { |
| 377 | + // Not sure how to build a URI with `magnet:` and not `magnet://` |
| 378 | + // without the `Uri::builder`. |
| 379 | + let mut uri = match &self.hash { |
| 380 | + InfoHash::V1(h) => format!("magnet:?xt=urn:btih:{h}"), |
| 381 | + InfoHash::V2(h) => format!("magnet:?xt=urn:btmh:1220{h}"), |
| 382 | + InfoHash::Hybrid((h1, h2)) => format!("magnet:?xt=urn:btih:{h1}&xt=urn:btmh:1220{h2}"), |
| 383 | + }; |
| 384 | + |
| 385 | + let mut buf = EString::<Query>::new(); |
| 386 | + |
| 387 | + if !self.name.is_empty() { |
| 388 | + buf.push_estr(EStr::new_or_panic("&dn=")); |
| 389 | + buf.encode_str::<Data>(&self.name); |
| 390 | + } |
| 391 | + |
| 392 | + // We use the helper to avoid duplicates or unsorted entries |
| 393 | + for tracker in self.trackers() { |
| 394 | + buf.push_estr(EStr::new_or_panic("&tr=")); |
| 395 | + buf.encode_str::<Data>(tracker.url()); |
| 396 | + } |
| 397 | + |
| 398 | + uri.push_str(buf.as_str()); |
| 399 | + |
| 400 | + MagnetLink::new(&uri) |
| 401 | + } |
331 | 402 | } |
332 | 403 |
|
333 | 404 | #[cfg(test)] |
@@ -452,4 +523,57 @@ mod tests { |
452 | 523 | let res = TorrentFile::from_slice(&slice); |
453 | 524 | assert!(res.is_err()); |
454 | 525 | } |
| 526 | + |
| 527 | + #[test] |
| 528 | + fn test_torrent_to_magnet_v2() { |
| 529 | + let slice = std::fs::read("tests/bittorrent-v2-test.torrent").unwrap(); |
| 530 | + let res = TorrentFile::from_slice(&slice); |
| 531 | + let torrent = res.unwrap(); |
| 532 | + |
| 533 | + let expected = std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap(); |
| 534 | + let magnet = torrent.magnet_link().unwrap(); |
| 535 | + assert_eq!(expected, magnet.to_string(),); |
| 536 | + } |
| 537 | + |
| 538 | + #[test] |
| 539 | + fn test_torrent_to_magnet_hybrid() { |
| 540 | + let slice = std::fs::read("tests/bittorrent-v2-hybrid-test.torrent").unwrap(); |
| 541 | + let res = TorrentFile::from_slice(&slice); |
| 542 | + let torrent = res.unwrap(); |
| 543 | + |
| 544 | + let expected = std::fs::read_to_string("tests/bittorrent-v2-hybrid-test.magnet").unwrap(); |
| 545 | + let magnet = torrent.magnet_link().unwrap(); |
| 546 | + assert_eq!(expected, magnet.to_string(),); |
| 547 | + } |
| 548 | + |
| 549 | + #[test] |
| 550 | + fn test_torrent_to_magnet_v1() { |
| 551 | + let slice = std::fs::read("tests/bittorrent-v1-emma-goldman.torrent").unwrap(); |
| 552 | + let res = TorrentFile::from_slice(&slice); |
| 553 | + let torrent = res.unwrap(); |
| 554 | + |
| 555 | + let expected = MagnetLink::new( |
| 556 | + &std::fs::read_to_string("tests/bittorrent-v1-emma-goldman.magnet").unwrap(), |
| 557 | + ) |
| 558 | + .unwrap(); |
| 559 | + let magnet = torrent.magnet_link().unwrap(); |
| 560 | + |
| 561 | + assert_eq!(expected.name(), magnet.name(),); |
| 562 | + |
| 563 | + assert_eq!(expected.hash(), magnet.hash(),); |
| 564 | + |
| 565 | + // Check for duplicates. This is useful because the `announce` in a torrent |
| 566 | + // file may also be in the `announce_list` so we wanna make sure we don't have it twice. |
| 567 | + for tracker in magnet.trackers() { |
| 568 | + if magnet.trackers().iter().filter(|x| x == &tracker).count() > 1 { |
| 569 | + panic!("Duplicate tracker: {tracker:?}"); |
| 570 | + } |
| 571 | + } |
| 572 | + |
| 573 | + // Check the trackers are actually equal |
| 574 | + assert_eq!(magnet.trackers(), expected.trackers()); |
| 575 | + |
| 576 | + // Check for complete equality in this specific case (no information loss on the test) |
| 577 | + assert_eq!(expected, magnet); |
| 578 | + } |
455 | 579 | } |
0 commit comments