Skip to main content

imdl/subcommand/torrent/
dump.rs

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    // flag is ok
116    let mut env = test_env! {
117      args: ["torrent", "dump", "--input", "foo"],
118      tree: {},
119    };
120    assert_matches!(env.run(), Err(Error::Filesystem { .. }));
121
122    // positional is ok
123    let mut env = test_env! {
124      args: ["torrent", "dump", "foo"],
125      tree: {},
126    };
127    assert_matches!(env.run(), Err(Error::Filesystem { .. }));
128
129    // both are fail
130    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}