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