feat(script): add options support

This commit is contained in:
Skylot
2022-07-21 20:39:05 +01:00
parent d9af91bc4d
commit 278d7fa3f9
10 changed files with 237 additions and 28 deletions
@@ -18,18 +18,20 @@ import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameterized;
import jadx.api.JadxDecompiler;
import jadx.api.impl.plugins.SimplePluginContext;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.JadxPluginManager;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.options.OptionDescription;
import jadx.core.utils.Utils;
public class JCommanderWrapper<T> {
private final JCommander jc;
private final JadxCLIArgs argsObj;
public JCommanderWrapper(T obj) {
this.jc = JCommander.newBuilder().addObject(obj).build();
public JCommanderWrapper(JadxCLIArgs argsObj) {
this.jc = JCommander.newBuilder().addObject(argsObj).build();
this.argsObj = argsObj;
}
public boolean parse(String[] args) {
@@ -43,7 +45,7 @@ public class JCommanderWrapper<T> {
}
}
public void overrideProvided(T obj) {
public void overrideProvided(JadxCLIArgs obj) {
List<ParameterDescription> fieldsParams = jc.getParameters();
List<ParameterDescription> parameters = new ArrayList<>(1 + fieldsParams.size());
parameters.add(jc.getMainParameterValue());
@@ -171,13 +173,19 @@ public class JCommanderWrapper<T> {
private String appendPluginOptions(int maxNamesLen) {
StringBuilder sb = new StringBuilder();
JadxPluginManager pluginManager = new JadxPluginManager();
pluginManager.load();
int k = 1;
for (JadxPlugin plugin : pluginManager.getAllPlugins()) {
if (plugin instanceof JadxPluginOptions) {
if (appendPlugin(((JadxPluginOptions) plugin), sb, maxNamesLen, k)) {
k++;
// load and init all options plugins to print all options
try (JadxDecompiler decompiler = new JadxDecompiler(argsObj.toJadxArgs())) {
Map<String, String> pluginOptions = decompiler.getArgs().getPluginOptions();
SimplePluginContext context = new SimplePluginContext(decompiler);
for (JadxPlugin plugin : decompiler.getPluginManager().getAllPlugins()) {
if (plugin instanceof JadxPluginOptions) {
JadxPluginOptions optionsPlugin = (JadxPluginOptions) plugin;
optionsPlugin.setOptions(pluginOptions);
optionsPlugin.init(context);
if (appendPlugin(optionsPlugin, sb, maxNamesLen, k)) {
k++;
}
}
}
}
@@ -116,7 +116,7 @@ public final class JadxDecompiler implements IJadxDecompiler, Closeable {
reset();
JadxArgsValidator.validate(this);
LOG.info("loading ...");
loadPlugins(args);
loadPlugins();
loadInputFiles();
root = new RootNode(args);
@@ -173,18 +173,18 @@ public final class JadxDecompiler implements IJadxDecompiler, Closeable {
loadedInputs.clear();
}
private void loadPlugins(JadxArgs args) {
private void loadPlugins() {
pluginManager.providesSuggestion("java-input", args.isUseDxInput() ? "java-convert" : "java-input");
pluginManager.load();
if (LOG.isDebugEnabled()) {
LOG.debug("Resolved plugins: {}", Utils.collectionMap(pluginManager.getResolvedPlugins(),
p -> p.getPluginInfo().getPluginId()));
}
applyPluginOptions(args);
applyPluginOptions();
initPlugins();
}
private void applyPluginOptions(JadxArgs args) {
private void applyPluginOptions() {
Map<String, String> pluginOptions = args.getPluginOptions();
if (!pluginOptions.isEmpty()) {
LOG.debug("Applying plugin options: {}", pluginOptions);
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import javax.swing.AbstractAction;
@@ -608,13 +609,12 @@ public class JadxSettingsWindow extends JDialog {
JadxPluginOptions optPlugin = (JadxPluginOptions) plugin;
for (OptionDescription opt : optPlugin.getOptionsDescriptions()) {
String title = "[" + pluginInfo.getPluginId() + "] " + opt.description();
if (opt.values().isEmpty()) {
JTextField textField = new JTextField();
textField.getDocument().addDocumentListener(new DocumentUpdateListener(event -> {
settings.getPluginOptions().put(opt.name(), textField.getText());
needReload();
}));
pluginsGroup.addRow(title, textField);
if (opt.values().isEmpty() || opt.getType() == OptionDescription.OptionType.BOOLEAN) {
try {
pluginsGroup.addRow(title, getPluginOptionEditor(opt));
} catch (Exception e) {
LOG.error("Failed to add editor for plugin option: {}", opt.name(), e);
}
} else {
String curValue = settings.getPluginOptions().get(opt.name());
JComboBox<String> combo = new JComboBox<>(opt.values().toArray(new String[0]));
@@ -630,6 +630,54 @@ public class JadxSettingsWindow extends JDialog {
return pluginsGroup;
}
private JComponent getPluginOptionEditor(OptionDescription opt) {
String curValue = settings.getPluginOptions().get(opt.name());
String value = curValue == null ? opt.defaultValue() : curValue;
switch (opt.getType()) {
case STRING:
JTextField textField = new JTextField();
textField.setText(value == null ? "" : value);
textField.getDocument().addDocumentListener(new DocumentUpdateListener(event -> {
settings.getPluginOptions().put(opt.name(), textField.getText());
needReload();
}));
return textField;
case NUMBER:
JSpinner numberField = new JSpinner();
numberField.setValue(safeStringToInt(value, 0));
numberField.addChangeListener(e -> {
settings.getPluginOptions().put(opt.name(), numberField.getValue().toString());
needReload();
});
return numberField;
case BOOLEAN:
JCheckBox boolField = new JCheckBox();
boolField.setSelected(Objects.equals(value, "yes") || Objects.equals(value, "true"));
boolField.addItemListener(e -> {
boolean editorValue = e.getStateChange() == ItemEvent.SELECTED;
settings.getPluginOptions().put(opt.name(), editorValue ? "yes" : "no");
needReload();
});
return boolField;
}
return null;
}
private int safeStringToInt(String value, int defValue) {
if (value == null) {
return defValue;
}
try {
return Integer.parseInt(value);
} catch (Exception e) {
LOG.warn("Failed parse string to int: {}", value, e);
return defValue;
}
}
private SettingsGroup makeOtherGroup() {
JComboBox<LangLocale> languageCbx = new JComboBox<>(NLS.getLangLocales());
for (LangLocale locale : NLS.getLangLocales()) {
@@ -22,4 +22,12 @@ public interface OptionDescription {
*/
@Nullable
String defaultValue();
enum OptionType {
STRING, NUMBER, BOOLEAN
}
default OptionType getType() {
return OptionType.STRING;
}
}
@@ -12,12 +12,18 @@ public class JadxOptionDescription implements OptionDescription {
private final String desc;
private final String defaultValue;
private final List<String> values;
private final OptionType type;
public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List<String> values) {
this(name, desc, defaultValue, values, OptionType.STRING);
}
public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List<String> values, OptionType type) {
this.name = name;
this.desc = desc;
this.defaultValue = defaultValue;
this.values = values;
this.type = type;
}
@Override
@@ -40,6 +46,11 @@ public class JadxOptionDescription implements OptionDescription {
return values;
}
@Override
public OptionType getType() {
return type;
}
@Override
public String toString() {
return "OptionDescription{" + desc + ", values=" + values + '}';
@@ -0,0 +1,30 @@
val jadx = getJadxInstance()
val testOpt = jadx.options.registerString(
"test",
"Simple string option",
values = listOf("first", "second"),
defaultValue = "first"
)
val numOpt = jadx.options.registerInt("number", "Number option").validate { it >= 0 }
val boolOpt = jadx.options.registerYesNo("bool", "Boolean option")
val allOptions = listOf(testOpt, numOpt, boolOpt)
jadx.afterLoad {
printOptions()
}
jadx.gui.ifAvailable {
addMenuAction("Print options") {
printOptions()
}
}
fun printOptions() {
allOptions.forEach { opt ->
println("Option: '${opt.name}', id: '${opt.id}', value: '${opt.value}'")
}
}
@@ -1,19 +1,26 @@
package jadx.plugins.script
import jadx.api.plugins.JadxPlugin
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.JadxPluginInfo
import jadx.api.plugins.gui.JadxGuiContext
import jadx.api.plugins.pass.JadxPassContext
import jadx.api.plugins.options.JadxPluginOptions
import jadx.api.plugins.options.OptionDescription
import jadx.plugins.script.passes.JadxScriptAfterLoadPass
import jadx.plugins.script.runner.ScriptEval
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
class JadxScriptPlugin : JadxPlugin {
class JadxScriptPlugin : JadxPluginOptions {
var scriptOptions: JadxScriptAllOptions = JadxScriptAllOptions(emptyMap())
override fun getPluginInfo() = JadxPluginInfo("jadx-script", "Jadx Script", "Scripting support for jadx")
override fun setOptions(options: Map<String, String>) {
scriptOptions = JadxScriptAllOptions(options)
}
override fun init(init: JadxPluginContext) {
val scriptStates = ScriptEval().process(init) ?: return
val scriptStates = ScriptEval().process(init, scriptOptions) ?: return
init.passContext.addPass(JadxScriptAfterLoadPass(scriptStates))
}
override fun getOptionsDescriptions(): List<OptionDescription> = scriptOptions.descriptions
}
@@ -4,6 +4,7 @@ import jadx.api.JadxDecompiler
import jadx.api.plugins.JadxPluginContext
import jadx.plugins.script.runtime.JadxScript
import jadx.plugins.script.runtime.JadxScriptData
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
import mu.KotlinLogging
import java.io.File
import kotlin.script.experimental.api.*
@@ -16,7 +17,7 @@ private val LOG = KotlinLogging.logger {}
class ScriptEval {
fun process(init: JadxPluginContext): ScriptStates? {
fun process(init: JadxPluginContext, scriptOptions: JadxScriptAllOptions): ScriptStates? {
val jadx = init.decompiler as JadxDecompiler
val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") }
if (scripts.isEmpty()) {
@@ -24,7 +25,7 @@ class ScriptEval {
}
val scriptStates = ScriptStates()
for (scriptFile in scripts) {
val scriptData = JadxScriptData(jadx, init, scriptFile)
val scriptData = JadxScriptData(jadx, init, scriptOptions, scriptFile)
load(scriptFile, scriptData)
scriptStates.add(scriptFile, scriptData)
}
@@ -0,0 +1,94 @@
package jadx.plugins.script.runtime.data
import jadx.api.plugins.options.OptionDescription
import jadx.api.plugins.options.OptionDescription.OptionType
import jadx.api.plugins.options.impl.JadxOptionDescription
import jadx.plugins.script.runtime.JadxScriptInstance
data class JadxScriptAllOptions(
val values: Map<String, String>,
val descriptions: MutableList<OptionDescription> = mutableListOf()
)
class ScriptOption<T>(
val name: String,
val id: String,
private val getter: () -> T,
) {
private var validate: ((T) -> Boolean)? = null
val value: T
get() {
val v = getter.invoke()
validate?.let { predicate ->
if (!predicate.invoke(v)) {
throw IllegalArgumentException("Invalid value '$v' for option $id")
}
}
return v
}
fun validate(predicate: (T) -> Boolean): ScriptOption<T> {
validate = predicate
return this
}
}
class JadxScriptOptions(
private val jadx: JadxScriptInstance,
private val options: JadxScriptAllOptions
) {
fun <T> register(
name: String,
desc: String,
values: List<String>,
defaultValue: String,
type: OptionType = OptionType.STRING,
convert: (String?) -> T
): ScriptOption<T> {
val id = "jadx-script.${jadx.scriptName}.$name"
options.descriptions.add(JadxOptionDescription(id, desc, defaultValue, values, type))
return ScriptOption(name, id) { convert.invoke(options.values[id]) }
}
fun registerString(
name: String,
desc: String = "",
values: List<String> = emptyList(),
defaultValue: String = ""
): ScriptOption<String> {
return register(name, desc, values, defaultValue) { value ->
if (value == null) {
defaultValue
} else {
if (values.isEmpty() || values.contains(value)) {
value
} else {
throw IllegalArgumentException("Unknown value '$value' for option '$name', expect one of $values")
}
}
}
}
fun registerYesNo(name: String, desc: String = "", defaultValue: Boolean = false): ScriptOption<Boolean> {
val defStr = if (defaultValue) "yes" else "no"
return register(name, desc, listOf("yes", "no"), defStr, OptionType.BOOLEAN) { value ->
when (value) {
null -> defaultValue
"yes", "true" -> true
"no", "false" -> false
else -> throw IllegalArgumentException("Unknown value '$value' for option '$name', expect: 'yes' or 'no'")
}
}
}
fun registerInt(name: String, desc: String = "", defaultValue: Int = 0): ScriptOption<Int> {
return register(name, desc, emptyList(), defaultValue.toString(), OptionType.NUMBER) { value ->
when (value) {
null -> defaultValue
else -> value.toInt()
}
}
}
}
@@ -31,6 +31,7 @@ open class JadxScriptBaseClass(private val scriptData: JadxScriptData) {
class JadxScriptData(
val jadxInstance: JadxDecompiler,
val pluginContext: JadxPluginContext,
val options: JadxScriptAllOptions,
val scriptFile: File
) {
val afterLoad: MutableList<() -> Unit> = ArrayList()
@@ -44,6 +45,7 @@ class JadxScriptInstance(
) {
private val decompiler = scriptData.jadxInstance
val options: JadxScriptOptions by lazy { JadxScriptOptions(this, scriptData.options) }
val rename: RenamePass by lazy { RenamePass(this) }
val stages: Stages by lazy { Stages(this) }
val replace: Replace by lazy { Replace(this) }