Skip to content

Commit fdbd032

Browse files
committed
feat: TorrentFile::trackers and TorrentFile::magnet_link methods
1 parent 8ab8e2a commit fdbd032

2 files changed

Lines changed: 130 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- `MagnetFile` supports serde de/serialization
1717
- `PeerSource`, `Tracker` and `TrackerScheme` now derive `Eq`, `PartialOrd`
1818
and `Ord` so that they can be sorted in collections
19+
- `TorrentFile::trackers` returns the trackers in the torrent file, sorted
20+
and deduplicated; the sort order is not specified
21+
- `TorrentFile::magnet_link` produces a `MagnetLink` that's roughly equivalent
22+
to the torrent file; some information may not be ported over, and this is
23+
a one-way operation
1924

2025
### Changed
2126

src/torrent_file.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
use bt_bencode::Value as BencodeValue;
2+
use fluent_uri::pct_enc::{
3+
encoder::{Data, Query},
4+
EStr, EString,
5+
};
26
use rustc_hex::ToHex;
37
#[cfg(feature = "sea_orm")]
48
use sea_orm::prelude::*;
@@ -8,7 +12,10 @@ use sha1::{Digest, Sha1};
812
use std::collections::HashMap;
913
use std::path::PathBuf;
1014

11-
use crate::{InfoHash, InfoHashError, PieceLength, TorrentContent, TorrentID, Tracker};
15+
use crate::{
16+
InfoHash, InfoHashError, MagnetLink, MagnetLinkError, PieceLength, TorrentContent, TorrentID,
17+
Tracker,
18+
};
1219

1320
/// Error occurred during parsing a [`TorrentFile`](crate::torrent_file::TorrentFile).
1421
#[derive(Clone, Debug, PartialEq)]
@@ -330,6 +337,70 @@ impl TorrentFile {
330337
pub fn id(&self) -> TorrentID {
331338
TorrentID::from_infohash(&self.hash)
332339
}
340+
341+
/// List the trackers in the torrent, ensuring
342+
/// they're sorted and deduplicated.
343+
pub fn trackers(&self) -> Vec<Tracker> {
344+
let mut trackers = vec![];
345+
if let Some(tracker) = &self.decoded.announce {
346+
trackers.push(tracker.clone());
347+
}
348+
349+
for tier in &self.decoded.announce_list {
350+
for tracker in tier {
351+
trackers.push(tracker.clone());
352+
}
353+
}
354+
355+
trackers.sort_unstable();
356+
trackers.dedup();
357+
358+
trackers
359+
}
360+
361+
/// Lossy transformation into a [`MagnetLink`].
362+
///
363+
/// This is a lossy operation because:
364+
///
365+
/// - magnet links do not contain detailed pieces information,
366+
/// so there is no reverse operation without network resolution
367+
/// - not all metadata about the torrent may be added to the magnet link
368+
///
369+
/// What's added to the magnet link:
370+
///
371+
/// - name
372+
/// - torrentID
373+
/// - trackers
374+
///
375+
/// For the moment, this is a fallible operation because we make sure
376+
/// the produced MagnetLink can be parsed again. This operation may not
377+
/// produce errors in a future release.
378+
pub fn magnet_link(&self) -> Result<MagnetLink, MagnetLinkError> {
379+
// Not sure how to build a URI with `magnet:` and not `magnet://`
380+
// without the `Uri::builder`.
381+
let mut uri = match &self.hash {
382+
InfoHash::V1(h) => format!("magnet:?xt=urn:btih:{h}"),
383+
InfoHash::V2(h) => format!("magnet:?xt=urn:btmh:1220{h}"),
384+
InfoHash::Hybrid((h1, h2)) => format!("magnet:?xt=urn:btih:{h1}&xt=urn:btmh:1220{h2}"),
385+
};
386+
387+
let mut buf = EString::<Query>::new();
388+
389+
if !self.name.is_empty() {
390+
buf.push_estr(EStr::new_or_panic("&dn="));
391+
buf.encode_str::<Data>(&self.name);
392+
}
393+
394+
// We use the helper to avoid duplicates or unsorted entries
395+
for tracker in self.trackers() {
396+
buf.push_estr(EStr::new_or_panic("&tr="));
397+
buf.encode_str::<Data>(tracker.url());
398+
}
399+
400+
uri.push_str(buf.as_str());
401+
402+
MagnetLink::new(&uri)
403+
}
333404
}
334405

335406
#[cfg(feature = "sea_orm")]
@@ -509,4 +580,57 @@ mod tests {
509580
let res = TorrentFile::from_slice(&slice);
510581
assert!(res.is_err());
511582
}
583+
584+
#[test]
585+
fn test_torrent_to_magnet_v2() {
586+
let slice = std::fs::read("tests/bittorrent-v2-test.torrent").unwrap();
587+
let res = TorrentFile::from_slice(&slice);
588+
let torrent = res.unwrap();
589+
590+
let expected = std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap();
591+
let magnet = torrent.magnet_link().unwrap();
592+
assert_eq!(expected, magnet.to_string(),);
593+
}
594+
595+
#[test]
596+
fn test_torrent_to_magnet_hybrid() {
597+
let slice = std::fs::read("tests/bittorrent-v2-hybrid-test.torrent").unwrap();
598+
let res = TorrentFile::from_slice(&slice);
599+
let torrent = res.unwrap();
600+
601+
let expected = std::fs::read_to_string("tests/bittorrent-v2-hybrid-test.magnet").unwrap();
602+
let magnet = torrent.magnet_link().unwrap();
603+
assert_eq!(expected, magnet.to_string(),);
604+
}
605+
606+
#[test]
607+
fn test_torrent_to_magnet_v1() {
608+
let slice = std::fs::read("tests/bittorrent-v1-emma-goldman.torrent").unwrap();
609+
let res = TorrentFile::from_slice(&slice);
610+
let torrent = res.unwrap();
611+
612+
let expected = MagnetLink::new(
613+
&std::fs::read_to_string("tests/bittorrent-v1-emma-goldman.magnet").unwrap(),
614+
)
615+
.unwrap();
616+
let magnet = torrent.magnet_link().unwrap();
617+
618+
assert_eq!(expected.name(), magnet.name(),);
619+
620+
assert_eq!(expected.hash(), magnet.hash(),);
621+
622+
// Check for duplicates. This is useful because the `announce` in a torrent
623+
// file may also be in the `announce_list` so we wanna make sure we don't have it twice.
624+
for tracker in magnet.trackers() {
625+
if magnet.trackers().iter().filter(|x| x == &tracker).count() > 1 {
626+
panic!("Duplicate tracker: {tracker:?}");
627+
}
628+
}
629+
630+
// Check the trackers are actually equal
631+
assert_eq!(magnet.trackers(), expected.trackers());
632+
633+
// Check for complete equality in this specific case (no information loss on the test)
634+
assert_eq!(expected, magnet);
635+
}
512636
}

0 commit comments

Comments
 (0)