feat(script): add options support
This commit is contained in:
@@ -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()) {
|
||||
|
||||
+8
@@ -22,4 +22,12 @@ public interface OptionDescription {
|
||||
*/
|
||||
@Nullable
|
||||
String defaultValue();
|
||||
|
||||
enum OptionType {
|
||||
STRING, NUMBER, BOOLEAN
|
||||
}
|
||||
|
||||
default OptionType getType() {
|
||||
return OptionType.STRING;
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -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}'")
|
||||
}
|
||||
}
|
||||
+12
-5
@@ -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
|
||||
}
|
||||
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
|
||||
+94
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user