imdl/subcommand/torrent/
show.rs

1use crate::common::*;
2
3const INPUT_HELP: &str = "Show information about torrent at `INPUT`. If `INPUT` is `-`, read \
4                          torrent metainfo from standard input.";
5
6const INPUT_FLAG: &str = "input-flag";
7
8const INPUT_POSITIONAL: &str = "<INPUT>";
9
10const INPUT_VALUE: &str = "INPUT";
11
12const JSON_OUTPUT: &str = "json";
13
14const JSON_HELP: &str = "Output data as JSON instead of the default format.";
15
16#[derive(StructOpt)]
17#[structopt(
18  help_message(consts::HELP_MESSAGE),
19  version_message(consts::VERSION_MESSAGE),
20  about("Display information about a .torrent file.")
21)]
22pub(crate) struct Show {
23  #[structopt(
24    name = INPUT_FLAG,
25    long = "input",
26    short = "i",
27    value_name = INPUT_VALUE,
28    empty_values(false),
29    parse(try_from_os_str = InputTarget::try_from_os_str),
30    help = INPUT_HELP,
31  )]
32  input_flag: Option<InputTarget>,
33  #[structopt(
34    name = INPUT_POSITIONAL,
35    value_name = INPUT_VALUE,
36    empty_values(false),
37    parse(try_from_os_str = InputTarget::try_from_os_str),
38    required_unless = INPUT_FLAG,
39    conflicts_with = INPUT_FLAG,
40    help = INPUT_HELP,
41  )]
42  input_positional: Option<InputTarget>,
43  #[structopt(
44    name = JSON_OUTPUT,
45    long = "json",
46    short = "j",
47    help = JSON_HELP,
48  )]
49  json: bool,
50}
51
52impl Show {
53  pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
54    let target = xor_args(
55      "input_flag",
56      self.input_flag.as_ref(),
57      "input_positional",
58      self.input_positional.as_ref(),
59    )?;
60
61    let input = env.read(target)?;
62    let summary = TorrentSummary::from_input(&input)?;
63    if self.json {
64      summary.write_json(env)?;
65    } else {
66      summary.write(env)?;
67    }
68    Ok(())
69  }
70}
71
72#[cfg(test)]
73mod tests {
74  use super::*;
75
76  use pretty_assertions::assert_eq;
77
78  #[test]
79  fn input_required() {
80    test_env! {
81      args: [
82        "torrent",
83        "show",
84      ],
85      tree: {
86      },
87      matches: Err(Error::Clap { .. }),
88    };
89
90    test_env! {
91      args: [
92        "torrent",
93        "show",
94        "--input",
95        "foo",
96      ],
97      tree: {
98      },
99      matches: Err(Error::Filesystem { .. }),
100    };
101
102    test_env! {
103      args: [
104        "torrent",
105        "show",
106        "foo",
107      ],
108      tree: {
109      },
110      matches: Err(Error::Filesystem { .. }),
111    };
112
113    test_env! {
114      args: [
115        "torrent",
116        "show",
117        "--input",
118        "foo",
119        "foo",
120      ],
121      tree: {
122      },
123      matches: Err(Error::Clap { .. }),
124    };
125  }
126
127  #[test]
128  fn output() -> Result<()> {
129    let metainfo = Metainfo::test_value_single();
130
131    #[rustfmt::skip]
132    let want_human_readable = format!(
133"         Name  NAME
134      Comment  COMMENT
135Creation Date  1970-01-01 00:00:01 UTC
136   Created By  CREATED BY
137       Source  SOURCE
138    Info Hash  {}
139 Torrent Size  {}
140 Content Size  32 KiB
141      Private  yes
142      Tracker  udp://announce.example:1337
143Announce List  Tier 1: http://a.example:4567
144                       https://b.example:77
145               Tier 2: udp://c.example:88
146   Update URL  https://update.example/
147    DHT Nodes  node.example:12
148               1.1.1.1:16
149               [2001:db8:85a3::8a2e:370]:7334
150   Piece Size  16 KiB
151  Piece Count  2
152   File Count  1
153        Files  NAME
154", Metainfo::test_value_single_infohash(), Metainfo::test_value_single_torrent_size());
155
156    #[rustfmt::skip]
157    let want_machine_readable = format!("\
158name\tNAME
159comment\tCOMMENT
160creation date\t1970-01-01 00:00:01 UTC
161created by\tCREATED BY
162source\tSOURCE
163info hash\t{}
164torrent size\t{}
165content size\t32768
166private\tyes
167tracker\tudp://announce.example:1337
168announce list\thttp://a.example:4567\thttps://b.example:77\tudp://c.example:88
169update url\thttps://update.example/
170dht nodes\tnode.example:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334
171piece size\t16384
172piece count\t2
173file count\t1
174files\tNAME
175", Metainfo::test_value_single_infohash(), Metainfo::test_value_single_torrent_size().count());
176
177    {
178      let mut env = TestEnvBuilder::new()
179        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
180        .out_is_term()
181        .build();
182
183      let path = env.resolve("foo.torrent")?;
184
185      metainfo.dump(path).unwrap();
186
187      env.assert_ok();
188
189      let have = env.out();
190      assert_eq!(have, want_human_readable);
191    }
192
193    {
194      let mut env = TestEnvBuilder::new()
195        .arg_slice(&["imdl", "torrent", "show", "foo.torrent"])
196        .out_is_term()
197        .build();
198
199      let path = env.resolve("foo.torrent")?;
200
201      metainfo.dump(path).unwrap();
202
203      env.assert_ok();
204
205      let have = env.out();
206
207      assert_eq!(have, want_human_readable);
208    }
209
210    {
211      let mut env = TestEnvBuilder::new()
212        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
213        .build();
214
215      let path = env.resolve("foo.torrent")?;
216
217      metainfo.dump(path).unwrap();
218
219      env.assert_ok();
220
221      let have = env.out();
222
223      assert_eq!(have, want_machine_readable);
224    }
225
226    Ok(())
227  }
228
229  #[test]
230  fn tier_list_with_main() -> Result<()> {
231    let metainfo = Metainfo::test_value_single();
232
233    {
234      let mut env = TestEnvBuilder::new()
235        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
236        .out_is_term()
237        .build();
238
239      let path = env.resolve("foo.torrent")?;
240
241      metainfo.dump(path).unwrap();
242
243      env.assert_ok();
244
245      let have = env.out();
246
247      #[rustfmt::skip]
248      let want = format!(
249        "         Name  NAME
250      Comment  COMMENT
251Creation Date  1970-01-01 00:00:01 UTC
252   Created By  CREATED BY
253       Source  SOURCE
254    Info Hash  {}
255 Torrent Size  {}
256 Content Size  32 KiB
257      Private  yes
258      Tracker  udp://announce.example:1337
259Announce List  Tier 1: http://a.example:4567
260                       https://b.example:77
261               Tier 2: udp://c.example:88
262   Update URL  https://update.example/
263    DHT Nodes  node.example:12
264               1.1.1.1:16
265               [2001:db8:85a3::8a2e:370]:7334
266   Piece Size  16 KiB
267  Piece Count  2
268   File Count  1
269        Files  NAME
270",
271        Metainfo::test_value_single_infohash(),
272        Metainfo::test_value_single_torrent_size()
273      );
274
275      assert_eq!(have, want);
276    }
277
278    {
279      let mut env = TestEnvBuilder::new()
280        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
281        .build();
282
283      let path = env.resolve("foo.torrent")?;
284
285      metainfo.dump(path).unwrap();
286
287      env.assert_ok();
288
289      let have = env.out();
290
291      #[rustfmt::skip]
292      let want = format!(
293        "\
294name\tNAME
295comment\tCOMMENT
296creation date\t1970-01-01 00:00:01 UTC
297created by\tCREATED BY
298source\tSOURCE
299info hash\t{}
300torrent size\t{}
301content size\t32768
302private\tyes
303tracker\tudp://announce.example:1337
304announce list\thttp://a.example:4567\thttps://b.example:77\tudp://c.example:88
305update url\thttps://update.example/
306dht nodes\tnode.example:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334
307piece size\t16384
308piece count\t2
309file count\t1
310files\tNAME
311",
312        Metainfo::test_value_single_infohash(),
313        Metainfo::test_value_single_torrent_size().count()
314      );
315
316      assert_eq!(have, want);
317    }
318
319    Ok(())
320  }
321
322  #[test]
323  fn tier_list_without_main() -> Result<()> {
324    let mut metainfo = Metainfo::test_value_single();
325
326    metainfo.announce_list = Some(vec![
327      vec!["B".into()],
328      vec!["C".into()],
329      vec!["ANNOUNCE".into()],
330    ]);
331
332    {
333      let mut env = TestEnvBuilder::new()
334        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
335        .out_is_term()
336        .build();
337
338      let path = env.resolve("foo.torrent")?;
339
340      metainfo.dump(path).unwrap();
341
342      env.assert_ok();
343
344      let have = env.out();
345
346      #[rustfmt::skip]
347      let want = format!(
348        "         Name  NAME
349      Comment  COMMENT
350Creation Date  1970-01-01 00:00:01 UTC
351   Created By  CREATED BY
352       Source  SOURCE
353    Info Hash  {}
354 Torrent Size  {}
355 Content Size  32 KiB
356      Private  yes
357      Tracker  udp://announce.example:1337
358Announce List  Tier 1: B
359               Tier 2: C
360               Tier 3: ANNOUNCE
361   Update URL  https://update.example/
362    DHT Nodes  node.example:12
363               1.1.1.1:16
364               [2001:db8:85a3::8a2e:370]:7334
365   Piece Size  16 KiB
366  Piece Count  2
367   File Count  1
368        Files  NAME
369",
370        Metainfo::test_value_single_infohash(),
371        Bytes(Metainfo::test_value_single_torrent_size().count() - 50)
372      );
373
374      assert_eq!(have, want);
375    }
376
377    {
378      let mut env = TestEnvBuilder::new()
379        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
380        .build();
381
382      let path = env.resolve("foo.torrent")?;
383
384      metainfo.dump(path).unwrap();
385
386      env.assert_ok();
387
388      let have = env.out();
389
390      #[rustfmt::skip]
391      let want = format!("\
392name\tNAME
393comment\tCOMMENT
394creation date\t1970-01-01 00:00:01 UTC
395created by\tCREATED BY
396source\tSOURCE
397info hash\t{}
398torrent size\t{}
399content size\t32768
400private\tyes
401tracker\tudp://announce.example:1337
402announce list\tB\tC\tANNOUNCE
403update url\thttps://update.example/
404dht nodes\tnode.example:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334
405piece size\t16384
406piece count\t2
407file count\t1
408files\tNAME
409",
410        Metainfo::test_value_single_infohash(),
411        Metainfo::test_value_single_torrent_size().count() - 50
412      );
413
414      assert_eq!(have, want);
415    }
416
417    Ok(())
418  }
419
420  #[test]
421  fn trackerless() -> Result<()> {
422    let mut metainfo = Metainfo::test_value_single();
423    metainfo.announce = None;
424    metainfo.announce_list = None;
425
426    {
427      let mut env = TestEnvBuilder::new()
428        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
429        .out_is_term()
430        .build();
431
432      let path = env.resolve("foo.torrent")?;
433
434      metainfo.dump(path).unwrap();
435
436      env.assert_ok();
437
438      let have = env.out();
439
440      #[rustfmt::skip]
441      let want = format!("         Name  NAME
442      Comment  COMMENT
443Creation Date  1970-01-01 00:00:01 UTC
444   Created By  CREATED BY
445       Source  SOURCE
446    Info Hash  {}
447 Torrent Size  {}
448 Content Size  32 KiB
449      Private  yes
450   Update URL  https://update.example/
451    DHT Nodes  node.example:12
452               1.1.1.1:16
453               [2001:db8:85a3::8a2e:370]:7334
454   Piece Size  16 KiB
455  Piece Count  2
456   File Count  1
457        Files  NAME
458",
459        Metainfo::test_value_single_infohash(),
460        Bytes(Metainfo::test_value_single_torrent_size().count() - 130)
461      );
462
463      assert_eq!(have, want);
464    }
465
466    {
467      let mut env = TestEnvBuilder::new()
468        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
469        .build();
470
471      let path = env.resolve("foo.torrent")?;
472
473      metainfo.dump(path).unwrap();
474
475      env.assert_ok();
476
477      let have = env.out();
478      #[rustfmt::skip]
479      let want = format!(
480        "\
481name\tNAME
482comment\tCOMMENT
483creation date\t1970-01-01 00:00:01 UTC
484created by\tCREATED BY
485source\tSOURCE
486info hash\t{}
487torrent size\t{}
488content size\t32768
489private\tyes
490update url\thttps://update.example/
491dht nodes\tnode.example:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334
492piece size\t16384
493piece count\t2
494file count\t1
495files\tNAME
496",
497        Metainfo::test_value_single_infohash(),
498        Metainfo::test_value_single_torrent_size().count() - 130
499      );
500
501      assert_eq!(have, want);
502    }
503
504    Ok(())
505  }
506
507  #[test]
508  fn unset() -> Result<()> {
509    let metainfo = Metainfo::test_value_single_unset();
510
511    {
512      let mut env = TestEnvBuilder::new()
513        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
514        .out_is_term()
515        .build();
516
517      let path = env.resolve("foo.torrent")?;
518
519      metainfo.dump(path).unwrap();
520
521      env.assert_ok();
522
523      let have = env.out();
524
525      #[rustfmt::skip]
526      let want = format!("        Name  NAME
527   Info Hash  {}
528Torrent Size  {}
529Content Size  5 bytes
530     Private  no
531  Piece Size  1 KiB
532 Piece Count  1
533  File Count  1
534       Files  NAME
535",
536        Metainfo::test_value_single_unset_infohash(),
537        Metainfo::test_value_single_unset_torrent_size()
538      );
539
540      assert_eq!(have, want);
541    }
542
543    {
544      let mut env = TestEnvBuilder::new()
545        .arg_slice(&["imdl", "torrent", "show", "--input", "foo.torrent"])
546        .build();
547
548      let path = env.resolve("foo.torrent")?;
549
550      metainfo.dump(path).unwrap();
551
552      env.assert_ok();
553
554      let have = env.out();
555      #[rustfmt::skip]
556      let want = format!(
557        "\
558name\tNAME
559info hash\t{}
560torrent size\t{}
561content size\t5
562private\tno
563piece size\t1024
564piece count\t1
565file count\t1
566files\tNAME
567",
568        Metainfo::test_value_single_unset_infohash(),
569        Metainfo::test_value_single_unset_torrent_size().count()
570      );
571
572      assert_eq!(have, want);
573    }
574
575    Ok(())
576  }
577
578  #[test]
579  fn output_json() {
580    {
581      let metainfo = Metainfo::test_value_single();
582      let mut want = r#"{"name":"NAME","comment":"COMMENT","creation_date":1,
583"created_by":"CREATED BY","source":"SOURCE","info_hash":"5d6f53772b4c20536fcce0c4c364d764a6efa39c",
584"torrent_size":509,"content_size":32768,"private":true,"tracker":
585"udp://announce.example:1337","announce_list":[["http://a.example:4567",
586"https://b.example:77"],["udp://c.example:88"]],"update_url":"https://update.example/",
587"dht_nodes":["node.example:12","1.1.1.1:16","[2001:db8:85a3::8a2e:370]:7334"],
588"piece_size":16384,"piece_count":2,"file_count":1,"files":["NAME"]}"#
589        .replace('\n', "");
590      want.push('\n');
591      let mut env = TestEnvBuilder::new()
592        .arg_slice(&[
593          "imdl",
594          "torrent",
595          "show",
596          "--input",
597          "foo.torrent",
598          "--json",
599        ])
600        .out_is_term()
601        .build();
602      let path = env.resolve("foo.torrent").unwrap();
603      metainfo.dump(path).unwrap();
604      env.assert_ok();
605      let have = env.out();
606      assert_eq!(have, want);
607    }
608
609    {
610      let metainfo = Metainfo::test_value_single_unset();
611      let mut want = r#"{"name":"NAME","comment":null,"creation_date":null,
612"created_by":null,"source":null,"info_hash":"a9105b0ff5f7cefeee5599ed7831749be21cc04e",
613"torrent_size":85,"content_size":5,"private":false,"tracker":null,"announce_list":[],
614"update_url":null,"dht_nodes":[],"piece_size":1024,"piece_count":1,"file_count":1,
615"files":["NAME"]}"#
616        .replace('\n', "");
617      want.push('\n');
618      let mut env = TestEnvBuilder::new()
619        .arg_slice(&[
620          "imdl",
621          "torrent",
622          "show",
623          "--input",
624          "foo.torrent",
625          "--json",
626        ])
627        .out_is_term()
628        .build();
629      let path = env.resolve("foo.torrent").unwrap();
630      metainfo.dump(path).unwrap();
631      env.assert_ok();
632      let have = env.out();
633      assert_eq!(have, want);
634    }
635  }
636}