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}