use crate::common::*; const INPUT_HELP: &str = "Generate magnet link from metainfo at `INPUT`. If `INPUT` is `-`, read \ metainfo from standard input."; const INPUT_FLAG: &str = "input-flag"; const INPUT_POSITIONAL: &str = ""; #[derive(StructOpt)] #[structopt( help_message(consts::HELP_MESSAGE), version_message(consts::VERSION_MESSAGE), about("Generate a magnet link from a .torrent file.") )] pub(crate) struct Link { #[structopt( name = INPUT_FLAG, long = "input", short = "i", value_name = "INPUT", empty_values(false), parse(try_from_os_str = InputTarget::try_from_os_str), help = INPUT_HELP, )] input_flag: Option, #[structopt( name = INPUT_POSITIONAL, value_name = "INPUT", empty_values(false), parse(try_from_os_str = InputTarget::try_from_os_str), required_unless = INPUT_FLAG, conflicts_with = INPUT_FLAG, help = INPUT_HELP, )] input_positional: Option, #[structopt( long = "open", short = "O", help = "Open generated magnet link. Uses `xdg-open`, `gnome-open`, or `kde-open` on Linux; \ `open` on macOS; and `cmd /C start` on Windows." )] open: bool, #[structopt( long = "peer", short = "p", value_name = "PEER", help = "Add `PEER` to magnet link." )] peers: Vec, #[structopt( long = "select-only", short = "s", value_name = "INDICES", use_delimiter = true, help = "Select files to download. Values are indices into the `info.files` list, e.g. \ `--select-only 1,2,3`." )] indices: Vec, } impl Link { pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { let input = xor_args( "input_flag", &self.input_flag, "input_positional", &self.input_positional, )?; let input = env.read(input)?; let infohash = Infohash::from_input(&input)?; let metainfo = Metainfo::from_input(&input)?; let mut link = MagnetLink::with_infohash(infohash); link.set_name(&metainfo.info.name); for result in metainfo.trackers() { link.add_tracker(result?); } for peer in self.peers { link.add_peer(peer); } for index in self.indices { link.add_index(index); } let url = link.to_url(); outln!(env, "{}", url)?; if self.open { Platform::open_url(&url)?; } Ok(()) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn input_required() { test_env! { args: [ "torrent", "link", ], tree: { }, matches: Err(Error::Clap { .. }), }; test_env! { args: [ "torrent", "link", "--input", "foo", ], tree: { }, matches: Err(Error::Filesystem { .. }), }; test_env! { args: [ "torrent", "link", "foo", ], tree: { }, matches: Err(Error::Filesystem { .. }), }; test_env! { args: [ "torrent", "link", "--input", "foo", "foo", ], tree: { }, matches: Err(Error::Clap { .. }), }; } #[test] fn no_announce_flag() { const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e"; let mut env = test_env! { args: [ "torrent", "link", "--input", "foo.torrent", ], tree: { "foo.torrent": "d4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:ee", } }; env.assert_ok(); let infohash = Sha1Digest::from_data(INFO.as_bytes()); assert_eq!( env.out(), format!("magnet:?xt=urn:btih:{}&dn=foo\n", infohash), ); } #[test] fn no_announce_positional() { const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e"; let mut env = test_env! { args: [ "torrent", "link", "foo.torrent", ], tree: { "foo.torrent": "d4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:ee", } }; env.assert_ok(); let infohash = Sha1Digest::from_data(INFO.as_bytes()); assert_eq!( env.out(), format!("magnet:?xt=urn:btih:{}&dn=foo\n", infohash), ); } #[test] fn with_announce() { const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e"; let mut env = test_env! { args: [ "torrent", "link", "--input", "foo.torrent", ], tree: { "foo.torrent": "d\ 8:announce24:https://foo.com/announce\ 4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\ e", } }; env.assert_ok(); let infohash = Sha1Digest::from_data(INFO.as_bytes()); assert_eq!( env.out(), format!( "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce\n", infohash ), ); } #[test] fn unique_trackers() { const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e"; let mut env = test_env! { args: [ "torrent", "link", "--input", "foo.torrent", ], tree: { "foo.torrent": "d\ 8:announce24:https://foo.com/announce\ 13:announce-listll24:https://foo.com/announceel24:https://bar.com/announceee\ 4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\ e", } }; env.assert_ok(); let infohash = Sha1Digest::from_data(INFO.as_bytes()); assert_eq!( env.out(), format!( "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&tr=https://bar.com/announce\n", infohash ), ); } #[test] fn with_peer() { const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e"; let mut env = test_env! { args: [ "torrent", "link", "--input", "foo.torrent", "--peer", "foo.com:1337", ], tree: { "foo.torrent": "d\ 8:announce24:https://foo.com/announce\ 4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\ e", } }; env.assert_ok(); let infohash = Sha1Digest::from_data(INFO.as_bytes()); assert_eq!( env.out(), format!( "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&x.pe=foo.com:1337\n", infohash ), ); } #[test] fn with_indices() { const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e"; let mut env = test_env! { args: [ "torrent", "link", "--input", "foo.torrent", "--select-only", "2,4", "--select-only", "4,6", ], tree: { "foo.torrent": "d\ 8:announce24:https://foo.com/announce\ 4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\ e", } }; env.assert_ok(); let infohash = Sha1Digest::from_data(INFO.as_bytes()); assert_eq!( env.out(), format!( "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&so=2,4,6\n", infohash ), ); } #[test] fn infohash_correct_with_nonstandard_info_dict() { const INFO: &str = "d1:ai0e6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e"; let mut env = test_env! { args: [ "torrent", "link", "--input", "foo.torrent", ], tree: { "foo.torrent": "d4:infod1:ai0e6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:ee", } }; env.assert_ok(); let infohash = Sha1Digest::from_data(INFO.as_bytes()); assert_eq!( env.out(), format!("magnet:?xt=urn:btih:{}&dn=foo\n", infohash), ); } #[test] fn bad_metainfo_error() { let mut env = test_env! { args: [ "torrent", "link", "--input", "foo.torrent", ], tree: { "foo.torrent": "i0e", } }; assert_matches!( env.run(), Err(Error::MetainfoValidate { input, source: MetainfoError::Type }) if input == "foo.torrent" ); } }