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}