1use crate::common::*;
2
3const METAINFO_HELP: &str = "Dump arbitrary bencode in `<INPUT>`. If `<INPUT>` is `-`, \
4 read metainfo from standard input.";
5
6const INPUT_POSITIONAL: &str = "<INPUT>";
7
8const INPUT_FLAG: &str = "input-flag";
9
10#[derive(StructOpt)]
11pub(crate) struct Dump {
12 #[structopt(
13 name = INPUT_FLAG,
14 long = "input",
15 short = "i",
16 value_name = "INPUT",
17 empty_values = false,
18 parse(try_from_os_str = InputTarget::try_from_os_str),
19 help = METAINFO_HELP,
20 )]
21 input_flag: Option<InputTarget>,
22 #[structopt(
23 name = INPUT_POSITIONAL,
24 value_name = "INPUT",
25 empty_values = false,
26 required_unless = INPUT_FLAG,
27 conflicts_with = INPUT_FLAG,
28 parse(try_from_os_str = InputTarget::try_from_os_str),
29 help = METAINFO_HELP,
30 )]
31 input_positional: Option<InputTarget>,
32}
33
34struct Fmt<'a>(&'a Value<'a>);
35
36fn fmt_string(f: &mut Formatter, string: &[u8]) -> fmt::Result {
37 if let Ok(string) = str::from_utf8(string) {
38 write!(f, "\"{string}\"")?;
39 } else {
40 write!(f, "0x")?;
41 for byte in string {
42 write!(f, "{byte:02x}")?;
43 }
44 }
45
46 Ok(())
47}
48
49impl Display for Fmt<'_> {
50 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
51 match &self.0 {
52 Value::Integer(integer) => write!(f, "{integer}")?,
53 Value::Dict(dict) => {
54 write!(f, "{{")?;
55
56 for (i, (key, value)) in dict.iter().enumerate() {
57 if i > 0 {
58 write!(f, ", ")?;
59 }
60 fmt_string(f, key)?;
61 write!(f, ": ")?;
62 write!(f, "{}", Fmt(value))?;
63 }
64
65 write!(f, "}}")?;
66 }
67 Value::List(list) => {
68 write!(f, "[")?;
69 for (i, element) in list.iter().enumerate() {
70 if i > 0 {
71 write!(f, ", ")?;
72 }
73 write!(f, "{}", Fmt(element))?;
74 }
75 write!(f, "]")?;
76 }
77 Value::Bytes(bytes) => fmt_string(f, bytes)?,
78 }
79
80 Ok(())
81 }
82}
83
84impl Dump {
85 pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
86 let target = xor_args(
87 "input_positional",
88 self.input_positional.as_ref(),
89 "input_flag",
90 self.input_flag.as_ref(),
91 )?;
92
93 let input = env.read(target.clone())?;
94
95 let value = Value::from_bencode(&input.data).unwrap();
96
97 outln!(env, "{}", Fmt(&value))?;
98
99 Ok(())
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn require_input() {
109 let mut env = test_env! {
110 args: ["torrent", "dump"],
111 tree: {},
112 };
113 assert_matches!(env.run(), Err(Error::Clap { .. }));
114
115 let mut env = test_env! {
117 args: ["torrent", "dump", "--input", "foo"],
118 tree: {},
119 };
120 assert_matches!(env.run(), Err(Error::Filesystem { .. }));
121
122 let mut env = test_env! {
124 args: ["torrent", "dump", "foo"],
125 tree: {},
126 };
127 assert_matches!(env.run(), Err(Error::Filesystem { .. }));
128
129 let mut env = test_env! {
131 args: ["torrent", "dump", "--input", "foo", "foo"],
132 tree: {},
133 };
134 assert_matches!(env.run(), Err(Error::Clap { .. }));
135 }
136
137 #[test]
138 fn hex_string() {
139 assert_eq!(
140 Fmt(&Value::Bytes(b"\x80\x81".to_vec().into())).to_string(),
141 "0x8081",
142 );
143 }
144
145 #[test]
146 fn output() {
147 fn case(input: &'static str, output: &str) {
148 let mut env = test_env! {
149 args: ["torrent", "dump", "input.torrent"],
150 tree: {
151 "input.torrent": input,
152 },
153 };
154
155 env.assert_ok();
156
157 assert_eq!(env.out(), output);
158 }
159
160 case("0:", "\"\"\n");
161 case("1:x", "\"x\"\n");
162
163 case("i-123e", "-123\n");
164 case("i-1e", "-1\n");
165 case("i0e", "0\n");
166 case("i123e", "123\n");
167
168 case("le", "[]\n");
169 case("li0ei1ei2ee", "[0, 1, 2]\n");
170
171 case("de", "{}\n");
172 case("d1:xi0ee", "{\"x\": 0}\n");
173 case("d1:xi0e1:yi1ee", "{\"x\": 0, \"y\": 1}\n");
174 case("d3:xyzd3:abci0eee", "{\"xyz\": {\"abc\": 0}}\n");
175 }
176}