Skip to main content

imdl/subcommand/torrent/
link.rs

1use crate::common::*;
2
3const INPUT_HELP: &str = "Generate magnet link from metainfo at `INPUT`. If `INPUT` is `-`, read \
4                          metainfo from standard input.";
5
6const INPUT_FLAG: &str = "input-flag";
7
8const INPUT_POSITIONAL: &str = "<INPUT>";
9
10#[derive(StructOpt)]
11#[structopt(
12  help_message(consts::HELP_MESSAGE),
13  version_message(consts::VERSION_MESSAGE),
14  about("Generate a magnet link from a .torrent file.")
15)]
16pub(crate) struct Link {
17  #[structopt(
18    name = INPUT_FLAG,
19    long = "input",
20    short = "i",
21    value_name = "INPUT",
22    empty_values(false),
23    parse(try_from_os_str = InputTarget::try_from_os_str),
24    help = INPUT_HELP,
25  )]
26  input_flag: Option<InputTarget>,
27  #[structopt(
28    name = INPUT_POSITIONAL,
29    value_name = "INPUT",
30    empty_values(false),
31    parse(try_from_os_str = InputTarget::try_from_os_str),
32    required_unless = INPUT_FLAG,
33    conflicts_with = INPUT_FLAG,
34    help = INPUT_HELP,
35  )]
36  input_positional: Option<InputTarget>,
37  #[structopt(
38    long = "open",
39    short = "O",
40    help = "Open generated magnet link. Uses `xdg-open`, `gnome-open`, or `kde-open` on Linux; \
41            `open` on macOS; and `cmd /C start` on Windows."
42  )]
43  open: bool,
44  #[structopt(
45    long = "peer",
46    short = "p",
47    value_name = "PEER",
48    help = "Add `PEER` to magnet link."
49  )]
50  peers: Vec<HostPort>,
51  #[structopt(
52    long = "select-only",
53    short = "s",
54    value_name = "INDICES",
55    use_delimiter = true,
56    help = "Select files to download. Values are indices into the `info.files` list, e.g. \
57            `--select-only 1,2,3`."
58  )]
59  indices: Vec<u64>,
60}
61
62impl Link {
63  pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
64    let input = xor_args(
65      "input_flag",
66      self.input_flag.as_ref(),
67      "input_positional",
68      self.input_positional.as_ref(),
69    )?;
70
71    let input = env.read(input)?;
72
73    let infohash = Infohash::from_input(&input)?;
74    let metainfo = Metainfo::from_input(&input)?;
75
76    let mut link = MagnetLink::with_infohash(infohash);
77
78    link.set_name(&metainfo.info.name);
79
80    for result in metainfo.trackers() {
81      link.add_tracker(result?);
82    }
83
84    for peer in self.peers {
85      link.add_peer(peer);
86    }
87
88    for index in self.indices {
89      link.add_index(index);
90    }
91
92    let url = link.to_url();
93
94    outln!(env, "{}", url)?;
95
96    if self.open {
97      Platform::open_url(&url)?;
98    }
99
100    Ok(())
101  }
102}
103
104#[cfg(test)]
105mod tests {
106  use super::*;
107
108  use pretty_assertions::assert_eq;
109
110  #[test]
111  fn input_required() {
112    test_env! {
113      args: [
114        "torrent",
115        "link",
116      ],
117      tree: {
118      },
119      matches: Err(Error::Clap { .. }),
120    };
121
122    test_env! {
123      args: [
124        "torrent",
125        "link",
126        "--input",
127        "foo",
128      ],
129      tree: {
130      },
131      matches: Err(Error::Filesystem { .. }),
132    };
133
134    test_env! {
135      args: [
136        "torrent",
137        "link",
138        "foo",
139      ],
140      tree: {
141      },
142      matches: Err(Error::Filesystem { .. }),
143    };
144
145    test_env! {
146      args: [
147        "torrent",
148        "link",
149        "--input",
150        "foo",
151        "foo",
152      ],
153      tree: {
154      },
155      matches: Err(Error::Clap { .. }),
156    };
157  }
158
159  #[test]
160  fn no_announce_flag() {
161    const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e";
162
163    let mut env = test_env! {
164      args: [
165        "torrent",
166        "link",
167        "--input",
168        "foo.torrent",
169      ],
170      tree: {
171        "foo.torrent": "d4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:ee",
172      }
173    };
174
175    env.assert_ok();
176
177    let infohash = Sha1Digest::from_data(INFO.as_bytes());
178
179    assert_eq!(
180      env.out(),
181      format!("magnet:?xt=urn:btih:{}&dn=foo\n", infohash),
182    );
183  }
184
185  #[test]
186  fn no_announce_positional() {
187    const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e";
188
189    let mut env = test_env! {
190      args: [
191        "torrent",
192        "link",
193        "foo.torrent",
194      ],
195      tree: {
196        "foo.torrent": "d4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:ee",
197      }
198    };
199
200    env.assert_ok();
201
202    let infohash = Sha1Digest::from_data(INFO.as_bytes());
203
204    assert_eq!(
205      env.out(),
206      format!("magnet:?xt=urn:btih:{}&dn=foo\n", infohash),
207    );
208  }
209
210  #[test]
211  fn with_announce() {
212    const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e";
213
214    let mut env = test_env! {
215      args: [
216        "torrent",
217        "link",
218        "--input",
219        "foo.torrent",
220      ],
221      tree: {
222        "foo.torrent": "d\
223          8:announce24:https://foo.com/announce\
224          4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\
225        e",
226      }
227    };
228
229    env.assert_ok();
230
231    let infohash = Sha1Digest::from_data(INFO.as_bytes());
232
233    assert_eq!(
234      env.out(),
235      format!(
236        "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce\n",
237        infohash
238      ),
239    );
240  }
241
242  #[test]
243  fn unique_trackers() {
244    const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e";
245
246    let mut env = test_env! {
247      args: [
248        "torrent",
249        "link",
250        "--input",
251        "foo.torrent",
252      ],
253      tree: {
254        "foo.torrent": "d\
255          8:announce24:https://foo.com/announce\
256          13:announce-listll24:https://foo.com/announceel24:https://bar.com/announceee\
257          4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\
258        e",
259      }
260    };
261
262    env.assert_ok();
263
264    let infohash = Sha1Digest::from_data(INFO.as_bytes());
265
266    assert_eq!(
267      env.out(),
268      format!(
269        "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&tr=https://bar.com/announce\n",
270        infohash
271      ),
272    );
273  }
274
275  #[test]
276  fn with_peer() {
277    const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e";
278
279    let mut env = test_env! {
280      args: [
281        "torrent",
282        "link",
283        "--input",
284        "foo.torrent",
285        "--peer",
286        "foo.com:1337",
287      ],
288      tree: {
289        "foo.torrent": "d\
290          8:announce24:https://foo.com/announce\
291          4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\
292        e",
293      }
294    };
295
296    env.assert_ok();
297
298    let infohash = Sha1Digest::from_data(INFO.as_bytes());
299
300    assert_eq!(
301      env.out(),
302      format!(
303        "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&x.pe=foo.com:1337\n",
304        infohash
305      ),
306    );
307  }
308
309  #[test]
310  fn with_indices() {
311    const INFO: &str = "d6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e";
312
313    let mut env = test_env! {
314      args: [
315        "torrent",
316        "link",
317        "--input",
318        "foo.torrent",
319        "--select-only",
320        "2,4",
321        "--select-only",
322        "4,6",
323      ],
324      tree: {
325        "foo.torrent": "d\
326          8:announce24:https://foo.com/announce\
327          4:infod6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e\
328        e",
329      }
330    };
331
332    env.assert_ok();
333
334    let infohash = Sha1Digest::from_data(INFO.as_bytes());
335
336    assert_eq!(
337      env.out(),
338      format!(
339        "magnet:?xt=urn:btih:{}&dn=foo&tr=https://foo.com/announce&so=2,4,6\n",
340        infohash
341      ),
342    );
343  }
344
345  #[test]
346  fn infohash_correct_with_nonstandard_info_dict() {
347    const INFO: &str = "d1:ai0e6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:e";
348
349    let mut env = test_env! {
350      args: [
351        "torrent",
352        "link",
353        "--input",
354        "foo.torrent",
355      ],
356      tree: {
357        "foo.torrent": "d4:infod1:ai0e6:lengthi0e4:name3:foo12:piece lengthi1e6:pieces0:ee",
358      }
359    };
360
361    env.assert_ok();
362
363    let infohash = Sha1Digest::from_data(INFO.as_bytes());
364
365    assert_eq!(
366      env.out(),
367      format!("magnet:?xt=urn:btih:{}&dn=foo\n", infohash),
368    );
369  }
370
371  #[test]
372  fn bad_metainfo_error() {
373    let mut env = test_env! {
374      args: [
375        "torrent",
376        "link",
377        "--input",
378        "foo.torrent",
379      ],
380      tree: {
381        "foo.torrent": "i0e",
382      }
383    };
384
385    assert_matches!(
386      env.run(), Err(Error::MetainfoValidate { input, source: MetainfoError::Type })
387      if input == "foo.torrent"
388    );
389  }
390}