1use std::fmt::Write as _;
4
5use crate::Metainfo;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct MagnetOptions {
10 display_name: bool,
11 trackers: bool,
12 web_seeds: bool,
13}
14
15impl Default for MagnetOptions {
16 fn default() -> Self {
17 Self {
18 display_name: true,
19 trackers: true,
20 web_seeds: true,
21 }
22 }
23}
24
25impl MagnetOptions {
26 #[must_use]
28 pub const fn builder() -> MagnetOptionsBuilder {
29 MagnetOptionsBuilder(Self {
30 display_name: true,
31 trackers: true,
32 web_seeds: true,
33 })
34 }
35}
36
37#[derive(Clone, Copy, Debug)]
39pub struct MagnetOptionsBuilder(MagnetOptions);
40
41impl MagnetOptionsBuilder {
42 #[must_use]
44 pub const fn display_name(mut self, include: bool) -> Self {
45 self.0.display_name = include;
46 self
47 }
48
49 #[must_use]
51 pub const fn trackers(mut self, include: bool) -> Self {
52 self.0.trackers = include;
53 self
54 }
55
56 #[must_use]
58 pub const fn web_seeds(mut self, include: bool) -> Self {
59 self.0.web_seeds = include;
60 self
61 }
62
63 #[must_use]
65 pub const fn build(self) -> MagnetOptions {
66 self.0
67 }
68}
69
70#[must_use]
72pub fn generate(metainfo: &Metainfo, options: &MagnetOptions) -> String {
73 let mut parameters = Vec::new();
74 if let Some(hash) = metainfo.info_hash_v1() {
75 parameters.push(format!("xt=urn:btih:{}", hash.hex()));
76 }
77 if let Some(hash) = metainfo.info_hash_v2() {
78 parameters.push(format!("xt=urn:btmh:1220{}", hash.hex()));
79 }
80 if options.display_name {
81 parameters.push(format!("dn={}", percent_encode(metainfo.name())));
82 }
83 if options.trackers {
84 for tracker in metainfo.trackers().iter().flatten() {
85 parameters.push(format!("tr={}", percent_encode(tracker)));
86 }
87 }
88 if options.web_seeds {
89 for seed in metainfo.web_seeds() {
90 parameters.push(format!("ws={}", percent_encode(seed)));
91 }
92 }
93 format!("magnet:?{}", parameters.join("&"))
94}
95
96fn percent_encode(bytes: &[u8]) -> String {
97 bytes.iter().fold(String::new(), |mut output, byte| {
98 if byte.is_ascii_alphanumeric() || matches!(*byte, b'-' | b'.' | b'_' | b'~') {
99 output.push(char::from(*byte));
100 } else {
101 write!(output, "%{byte:02X}").expect("writing to String cannot fail");
102 }
103 output
104 })
105}