11use bt_bencode:: Value as BencodeValue ;
2+ use fluent_uri:: pct_enc:: {
3+ encoder:: { Data , Query } ,
4+ EStr , EString ,
5+ } ;
26use rustc_hex:: ToHex ;
37#[ cfg( feature = "sea_orm" ) ]
48use sea_orm:: prelude:: * ;
@@ -8,7 +12,10 @@ use sha1::{Digest, Sha1};
812use std:: collections:: HashMap ;
913use 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