Skip to content

Commit 7f721c1

Browse files
committed
feat: TorrentFile::trackers and TorrentFile::magnet_link methods
1 parent 7ab4933 commit 7f721c1

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,12 +1,19 @@
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
use serde::{Deserialize, Serialize};
48
use sha1::{Digest, Sha1};
59

610
use std::collections::HashMap;
711
use std::path::PathBuf;
812

9-
use crate::{InfoHash, InfoHashError, PieceLength, TorrentContent, TorrentID, Tracker};
13+
use crate::{
14+
InfoHash, InfoHashError, MagnetLink, MagnetLinkError, PieceLength, TorrentContent, TorrentID,
15+
Tracker,
16+
};
1017

1118
/// Error occurred during parsing a [`TorrentFile`](crate::torrent_file::TorrentFile).
1219
#[derive(Clone, Debug, PartialEq)]
@@ -328,6 +335,70 @@ impl TorrentFile {
328335
pub fn id(&self) -> TorrentID {
329336
TorrentID::from_infohash(&self.hash)
330337
}
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+
}
331402
}
332403

333404
#[cfg(test)]
@@ -452,4 +523,57 @@ mod tests {
452523
let res = TorrentFile::from_slice(&slice);
453524
assert!(res.is_err());
454525
}
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+
}
455579
}

0 commit comments

Comments
 (0)