From 278d7fa3f932e5a183cdf53cd0278a4fb6bf239e Mon Sep 17 00:00:00 2001 From: Skylot Date: Thu, 21 Jul 2022 20:39:05 +0100 Subject: [PATCH] feat(script): add options support --- .../main/java/jadx/cli/JCommanderWrapper.java | 28 ++++-- .../main/java/jadx/api/JadxDecompiler.java | 8 +- .../jadx/gui/settings/JadxSettingsWindow.java | 62 ++++++++++-- .../plugins/options/OptionDescription.java | 8 ++ .../options/impl/JadxOptionDescription.java | 11 +++ .../examples/scripts/options.jadx.kts | 30 ++++++ .../jadx/plugins/script/JadxScriptPlugin.kt | 17 +++- .../jadx/plugins/script/runner/ScriptEval.kt | 5 +- .../plugins/script/runtime/data/options.kt | 94 +++++++++++++++++++ .../jadx/plugins/script/runtime/runtime.kt | 2 + 10 files changed, 237 insertions(+), 28 deletions(-) create mode 100644 jadx-plugins/jadx-script/examples/scripts/options.jadx.kts create mode 100644 jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/options.kt diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index d582b3179..febf7d71b 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -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 { 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 { } } - public void overrideProvided(T obj) { + public void overrideProvided(JadxCLIArgs obj) { List fieldsParams = jc.getParameters(); List parameters = new ArrayList<>(1 + fieldsParams.size()); parameters.add(jc.getMainParameterValue()); @@ -171,13 +173,19 @@ public class JCommanderWrapper { 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 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++; + } } } } diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 909aa6bbc..64f07807b 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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 pluginOptions = args.getPluginOptions(); if (!pluginOptions.isEmpty()) { LOG.debug("Applying plugin options: {}", pluginOptions); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index ab748e029..353a5fdd3 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -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 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 languageCbx = new JComboBox<>(NLS.getLangLocales()); for (LangLocale locale : NLS.getLangLocales()) { diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/OptionDescription.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/OptionDescription.java index 04eb83a06..62355c137 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/OptionDescription.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/OptionDescription.java @@ -22,4 +22,12 @@ public interface OptionDescription { */ @Nullable String defaultValue(); + + enum OptionType { + STRING, NUMBER, BOOLEAN + } + + default OptionType getType() { + return OptionType.STRING; + } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java index ff291872c..b913c1dad 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java @@ -12,12 +12,18 @@ public class JadxOptionDescription implements OptionDescription { private final String desc; private final String defaultValue; private final List values; + private final OptionType type; public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List values) { + this(name, desc, defaultValue, values, OptionType.STRING); + } + + public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List 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 + '}'; diff --git a/jadx-plugins/jadx-script/examples/scripts/options.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/options.jadx.kts new file mode 100644 index 000000000..93d554f64 --- /dev/null +++ b/jadx-plugins/jadx-script/examples/scripts/options.jadx.kts @@ -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}'") + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt index be3c37331..2b8a2166d 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/JadxScriptPlugin.kt @@ -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) { + 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 = scriptOptions.descriptions } diff --git a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt index 20a868750..b94e9a5de 100644 --- a/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt +++ b/jadx-plugins/jadx-script/jadx-script-plugin/src/main/kotlin/jadx/plugins/script/runner/ScriptEval.kt @@ -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) } diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/options.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/options.kt new file mode 100644 index 000000000..afa88a5ce --- /dev/null +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/options.kt @@ -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, + val descriptions: MutableList = mutableListOf() +) + +class ScriptOption( + 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 { + validate = predicate + return this + } +} + +class JadxScriptOptions( + private val jadx: JadxScriptInstance, + private val options: JadxScriptAllOptions +) { + + fun register( + name: String, + desc: String, + values: List, + defaultValue: String, + type: OptionType = OptionType.STRING, + convert: (String?) -> T + ): ScriptOption { + 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 = emptyList(), + defaultValue: String = "" + ): ScriptOption { + 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 { + 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 { + return register(name, desc, emptyList(), defaultValue.toString(), OptionType.NUMBER) { value -> + when (value) { + null -> defaultValue + else -> value.toInt() + } + } + } +} diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/runtime.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/runtime.kt index 56202f10e..4d9fab1b4 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/runtime.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/runtime.kt @@ -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) }