1use super::{ConfigError, expand_home, parse_duration};
7use std::collections::BTreeMap;
8use std::path::PathBuf;
9use std::time::Duration;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13#[non_exhaustive]
14pub struct Decompiler {
15 pub command: Option<PathBuf>,
19 pub args: Vec<String>,
21 pub env: BTreeMap<String, String>,
23 pub cwd: Option<PathBuf>,
25 pub instances: usize,
27 pub max_concurrency: Option<u32>,
30 pub idle: Duration,
32 pub timeout: Duration,
34 pub startup_timeout: Duration,
36 pub inputs: Vec<String>,
39 pub options: serde_json::Map<String, serde_json::Value>,
41 pub enabled: bool,
43}
44
45const KEYS: &[&str] = &[
47 "command",
48 "args",
49 "env",
50 "cwd",
51 "instances",
52 "max_concurrency",
53 "idle",
54 "timeout",
55 "startup_timeout",
56 "inputs",
57 "options",
58 "enabled",
59];
60
61pub const BUILT_IN_DECOMPILER: &str = "dexdec";
63
64const INPUT_KINDS: &[&str] = &["archive", "slice"];
66
67impl Decompiler {
68 pub fn new(command: impl Into<PathBuf>) -> Decompiler {
70 Decompiler {
71 command: Some(command.into()),
72 args: Vec::new(),
73 env: BTreeMap::new(),
74 cwd: None,
75 instances: 1,
76 max_concurrency: None,
77 idle: Duration::from_secs(10 * 60),
78 timeout: Duration::from_secs(60),
79 startup_timeout: Duration::from_secs(30),
80 inputs: Vec::new(),
81 options: serde_json::Map::new(),
82 enabled: true,
83 }
84 }
85
86 pub(super) fn parse_all(value: &toml::Value, origin: &str) -> Result<BTreeMap<String, Decompiler>, ConfigError> {
88 let invalid =
89 |key: String, text: String, expected: &'static str| ConfigError::Invalid { origin: origin.into(), key, text, expected };
90 let toml::Value::Table(tables) = value else {
91 return Err(invalid("decompilers".into(), format!("<{}>", value.type_str()), "a table of [decompilers.NAME] tables"));
92 };
93 tables
94 .iter()
95 .map(|(name, table)| {
96 let toml::Value::Table(table) = table else {
97 return Err(invalid(format!("decompilers.{name}"), format!("<{}>", table.type_str()), "a table"));
98 };
99 Ok((name.clone(), Decompiler::parse(name, table, origin)?))
100 })
101 .collect()
102 }
103
104 fn parse(name: &str, table: &toml::Table, origin: &str) -> Result<Decompiler, ConfigError> {
105 let invalid = |key: &str, value: &toml::Value, expected: &'static str| ConfigError::Invalid {
106 origin: origin.into(),
107 key: format!("decompilers.{name}.{key}"),
108 text: match value {
109 toml::Value::String(s) => s.clone(),
110 toml::Value::Integer(n) => n.to_string(),
111 toml::Value::Float(x) => x.to_string(),
112 toml::Value::Boolean(b) => b.to_string(),
113 other => format!("<{}>", other.type_str()),
114 },
115 expected,
116 };
117 let command = match table.get("command") {
118 Some(command) => Some(command.as_str().filter(|c| !c.is_empty()).ok_or_else(|| invalid("command", command, "a command"))?),
119 None if name == BUILT_IN_DECOMPILER => None,
120 None => {
121 return Err(ConfigError::Invalid {
122 origin: origin.into(),
123 key: format!("decompilers.{name}.command"),
124 text: String::new(),
125 expected: "a command (required but for the built-in dexdec)",
126 });
127 }
128 };
129 if command.is_none()
130 && let Some(args) = table.get("args")
131 {
132 return Err(invalid("args", args, "no args without a command (the built-in dexdec takes none)"));
133 }
134 let mut decompiler = Decompiler::new("");
135 decompiler.command = command.map(expand_home);
136 for (key, value) in table {
137 let strings = || -> Option<Vec<String>> { value.as_array()?.iter().map(|v| v.as_str().map(String::from)).collect() };
138 let duration = |expected| {
139 let parsed = match value {
140 toml::Value::Integer(n) => u64::try_from(*n).ok().map(Duration::from_secs),
141 toml::Value::String(s) => parse_duration(s, Duration::from_secs(1)),
142 _ => None,
143 };
144 parsed.ok_or_else(|| invalid(key, value, expected))
145 };
146 const DURATION: &str = "a duration (60, 30s, 10m; a bare number is seconds)";
147 match key.as_str() {
148 "command" => {}
149 "args" => decompiler.args = strings().ok_or_else(|| invalid(key, value, "an array of strings"))?,
150 "env" => {
151 let env = value.as_table().and_then(|t| t.iter().map(|(k, v)| Some((k.clone(), v.as_str()?.to_string()))).collect());
152 decompiler.env = env.ok_or_else(|| invalid(key, value, "a table of strings"))?;
153 }
154 "cwd" => {
155 let cwd = value.as_str().filter(|c| !c.is_empty()).ok_or_else(|| invalid(key, value, "a directory"))?;
156 decompiler.cwd = Some(expand_home(cwd));
157 }
158 "instances" => {
159 let n = value.as_integer().and_then(|n| usize::try_from(n).ok()).filter(|&n| n > 0);
160 decompiler.instances = n.ok_or_else(|| invalid(key, value, "a number of processes, at least 1"))?;
161 }
162 "max_concurrency" => {
163 let n = value.as_integer().and_then(|n| u32::try_from(n).ok()).filter(|&n| n > 0);
164 decompiler.max_concurrency = Some(n.ok_or_else(|| invalid(key, value, "a number of requests, at least 1"))?);
165 }
166 "idle" => decompiler.idle = duration(DURATION)?,
167 "timeout" => decompiler.timeout = duration(DURATION)?,
168 "startup_timeout" => decompiler.startup_timeout = duration(DURATION)?,
169 "inputs" => {
170 let kinds = strings().filter(|kinds| kinds.iter().all(|kind| INPUT_KINDS.contains(&kind.as_str())));
171 decompiler.inputs = kinds.ok_or_else(|| invalid(key, value, "an array of archive, slice"))?;
172 }
173 "options" => {
174 let options = value.as_table().and_then(|t| serde_json::to_value(t).ok()).and_then(|v| match v {
175 serde_json::Value::Object(map) => Some(map),
176 _ => None,
177 });
178 decompiler.options = options.ok_or_else(|| invalid(key, value, "a table"))?;
179 }
180 "enabled" => decompiler.enabled = value.as_bool().ok_or_else(|| invalid(key, value, "true or false"))?,
181 unknown => {
182 return Err(ConfigError::UnknownKey {
183 origin: origin.into(),
184 key: format!("decompilers.{name}.{unknown}"),
185 known: KEYS.join(", "),
186 });
187 }
188 }
189 }
190 Ok(decompiler)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::config::Config;
198 use std::path::Path;
199
200 fn load(text: &str) -> Result<Config, ConfigError> {
201 Config::resolve(Some((Path::new("c.toml"), text)), |_| None, &[])
202 }
203
204 #[test]
205 fn tables_read_with_defaults_and_pass_options_through() {
206 let c = load(
207 r#"
208 threads = 2
209 [decompile]
210 default = "jeb"
211 fallback = ["dexdec", "jadx"]
212 [decompilers.jeb]
213 command = "java"
214 args = ["-Xmx6g", "-jar", "/opt/jeb-server.jar", "--stdio"]
215 env = { JEB_HOME = "/opt/jeb" }
216 instances = 2
217 max_concurrency = 1
218 idle = "5m"
219 timeout = 90
220 startup_timeout = "1m"
221 inputs = ["slice"]
222 options = { finally_rebuilder = true, depth = 3 }
223 [decompilers.jadx]
224 command = "jadx-worker"
225 enabled = false
226 "#,
227 )
228 .unwrap();
229 assert_eq!(c.decompile_default.value.0, "jeb");
230 assert_eq!(c.decompile_fallback.value.0, ["dexdec", "jadx"]);
231 let jeb = &c.decompilers["jeb"];
232 assert_eq!(jeb.command.as_deref(), Some(Path::new("java")));
233 assert_eq!(jeb.args.len(), 4);
234 assert_eq!(jeb.env["JEB_HOME"], "/opt/jeb");
235 assert_eq!((jeb.instances, jeb.max_concurrency), (2, Some(1)));
236 assert_eq!(
237 (jeb.idle, jeb.timeout, jeb.startup_timeout),
238 (Duration::from_secs(300), Duration::from_secs(90), Duration::from_secs(60))
239 );
240 assert_eq!(jeb.inputs, ["slice"]);
241 assert_eq!(serde_json::Value::Object(jeb.options.clone()), serde_json::json!({"finally_rebuilder": true, "depth": 3}));
242 let jadx = &c.decompilers["jadx"];
243 assert!(!jadx.enabled);
244 assert_eq!((jadx.instances, jadx.timeout, jadx.cwd.as_ref()), (1, Duration::from_secs(60), None));
245 }
246
247 #[test]
248 fn bad_tables_name_the_key() {
249 let cases = [
250 ("[decompilers.x]\nargs = []\n", "decompilers.x.command"),
251 ("[decompilers.x]\ncommand = \"x\"\ninstances = 0\n", "decompilers.x.instances = \"0\": expected a number of processes"),
252 ("[decompilers.x]\ncommand = \"x\"\ntimeout = \"soon\"\n", "decompilers.x.timeout = \"soon\": expected a duration"),
253 ("[decompilers.x]\ncommand = \"x\"\ninputs = [\"apk\"]\n", "expected an array of archive, slice"),
254 ("[decompilers.x]\ncommand = \"x\"\nmaxConcurrency = 1\n", "unknown setting \"decompilers.x.maxConcurrency\""),
255 ("[decompile]\nfallback = [1]\n", "decompile.fallback = \"<array>\": expected an array of strings"),
256 ("decompilers = 1\n", "expected a table of [decompilers.NAME] tables"),
257 ("[decompilers.dexdec]\nargs = [\"x\"]\n", "decompilers.dexdec.args"),
258 ];
259 for (text, want) in cases {
260 let e = load(text).unwrap_err().to_string();
261 assert!(e.starts_with("c.toml: ") && e.contains(want), "{text:?}: {e}");
262 }
263 }
264
265 #[test]
266 fn the_built_in_backend_needs_no_command() {
267 let c = load("[decompilers.dexdec]\noptions = { indent = \" \" }\ninstances = 2\n").unwrap();
268 let dexdec = &c.decompilers[BUILT_IN_DECOMPILER];
269 assert_eq!((dexdec.command.as_ref(), dexdec.instances), (None, 2));
270 assert_eq!(dexdec.options["indent"], " ");
271 }
272
273 #[test]
274 fn decompile_settings_come_from_any_layer() {
275 let c = Config::resolve(None, |_| None, &[]).unwrap();
276 assert_eq!((c.decompile_default.value.0.as_str(), c.decompile_fallback.value.0.len()), ("dexdec", 0));
277 assert_eq!(c.decompile_mapping.value.0, None);
278 let env = |name: &str| (name == "ASC_DECOMPILE_FALLBACK").then(|| "jadx, jeb".to_string());
279 let flags = [("decompile.mapping".to_string(), "/m/mapping.txt".to_string())];
280 let c = Config::resolve(None, env, &flags).unwrap();
281 assert_eq!(c.decompile_fallback.value.0, ["jadx", "jeb"]);
282 assert_eq!(c.decompile_mapping.value.0.as_deref(), Some(Path::new("/m/mapping.txt")));
283 let c = load("[decompile]\nfallback = []\n").unwrap();
284 assert!(c.decompile_fallback.value.0.is_empty());
285 }
286}