391 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			391 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
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 = "<INPUT>";
 | 
						|
 | 
						|
#[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<InputTarget>,
 | 
						|
  #[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<InputTarget>,
 | 
						|
  #[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<HostPort>,
 | 
						|
  #[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<u64>,
 | 
						|
}
 | 
						|
 | 
						|
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"
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |