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
@@ -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) }