feat: add base scripting support

This commit is contained in:
Skylot
2022-07-09 17:57:44 +01:00
parent fdf170529f
commit e5e64365fc
64 changed files with 1488 additions and 53 deletions
@@ -0,0 +1,19 @@
package jadx.plugins.script.runtime.data
import jadx.api.core.nodes.IMethodNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.dex.visitors.DotGraphVisitor
import jadx.core.utils.DebugUtils
import jadx.plugins.script.runtime.JadxScriptInstance
import java.io.File
class Debug(private val jadx: JadxScriptInstance) {
fun printMethodRegions(mth: IMethodNode, printInsns: Boolean = false) {
DebugUtils.printRegions(mth as MethodNode, printInsns)
}
fun saveCFG(mth: IMethodNode, file: File = File("dump-mth-raw")) {
DotGraphVisitor.dumpRaw().save(file, mth as MethodNode)
}
}
@@ -0,0 +1,24 @@
package jadx.plugins.script.runtime.data
import jadx.api.JadxArgs
import jadx.api.JavaClass
import jadx.plugins.script.runtime.JadxScriptInstance
import java.util.concurrent.Executors
class Decompile(private val jadx: JadxScriptInstance) {
fun all() {
jadx.classes.forEach(JavaClass::decompile)
}
fun allThreaded(threadsCount: Int = JadxArgs.DEFAULT_THREADS_COUNT) {
val executor = Executors.newFixedThreadPool(threadsCount)
val dec = jadx.internalDecompiler
val batches = dec.decompileScheduler.buildBatches(jadx.classes)
for (batch in batches) {
executor.submit {
batch.forEach(JavaClass::decompile)
}
}
}
}
@@ -0,0 +1,24 @@
package jadx.plugins.script.runtime.data
import jadx.api.plugins.gui.JadxGuiContext
import jadx.plugins.script.runtime.JadxScriptInstance
class Gui(
private val jadx: JadxScriptInstance,
private val guiContext: JadxGuiContext?
) {
fun isAvailable() = guiContext != null
fun ifAvailable(block: Gui.() -> Unit) {
guiContext?.let { this.apply(block) }
}
fun ui(block: () -> Unit) {
guiContext?.uiRun(block)
}
fun addMenuAction(name: String, action: () -> Unit) {
guiContext?.addMenuAction(name, action)
}
}
@@ -0,0 +1,39 @@
package jadx.plugins.script.runtime.data
import jadx.api.core.nodes.IRootNode
import jadx.core.dex.nodes.IDexNode
import jadx.core.dex.nodes.RootNode
import jadx.plugins.script.runtime.JadxScriptInstance
class RenamePass(private val jadx: JadxScriptInstance) {
fun all(makeNewName: (String) -> String?) {
all { name, _ -> makeNewName.invoke(name) }
}
fun all(makeNewName: (String, IDexNode) -> String?) {
jadx.addPass(object : ScriptPreparePass(jadx, "RenameAll") {
override fun init(root: IRootNode) {
val rootNode = root as RootNode
for (cls in rootNode.classes) {
makeNewName.invoke(cls.classInfo.shortName, cls)?.let {
cls.classInfo.changeShortName(it)
}
for (mth in cls.methods) {
if (mth.isConstructor) {
continue
}
makeNewName.invoke(mth.name, mth)?.let {
mth.rename(it)
}
}
for (fld in cls.fields) {
makeNewName.invoke(fld.name, fld)?.let {
fld.fieldInfo.alias = it
}
}
}
}
})
}
}
@@ -0,0 +1,43 @@
package jadx.plugins.script.runtime.data
import jadx.core.dex.instructions.args.InsnArg
import jadx.core.dex.instructions.args.InsnWrapArg
import jadx.core.dex.nodes.InsnNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.utils.InsnRemover
import jadx.plugins.script.runtime.JadxScriptInstance
class Replace(private val jadx: JadxScriptInstance) {
fun insns(replace: (MethodNode, InsnNode) -> InsnNode?) {
jadx.stages.mthBlocks { mth, blocks ->
for (block in blocks) {
val insns = block.instructions
for ((i, insn) in insns.withIndex()) {
replaceSubInsns(mth, insn, replace)
replace.invoke(mth, insn)?.let {
insns[i] = it
}
}
}
}
}
private fun replaceSubInsns(mth: MethodNode, insn: InsnNode, replace: (MethodNode, InsnNode) -> InsnNode?) {
val argsCount = insn.argsCount
if (argsCount == 0) {
return
}
for (i in 0 until argsCount) {
val arg = insn.getArg(i)
if (arg is InsnWrapArg) {
val wrapInsn = arg.wrapInsn
replaceSubInsns(mth, wrapInsn, replace)
replace.invoke(mth, wrapInsn)?.let {
InsnRemover.unbindArgUsage(mth, arg)
insn.setArg(i, InsnArg.wrapInsnIntoArg(it))
}
}
}
}
}
@@ -0,0 +1,16 @@
package jadx.plugins.script.runtime.data
import jadx.core.dex.nodes.ClassNode
import jadx.plugins.script.runtime.JadxScriptInstance
class Search(private val jadx: JadxScriptInstance) {
private val dec = jadx.internalDecompiler
fun classByFullName(fullName: String): ClassNode? {
return dec.searchClassNodeByOrigFullName(fullName)
}
fun classesByShortName(fullName: String): List<ClassNode> {
return dec.root.searchClassByShortName(fullName)
}
}
@@ -0,0 +1,63 @@
package jadx.plugins.script.runtime.data
import jadx.api.core.nodes.IMethodNode
import jadx.core.dex.nodes.BlockNode
import jadx.core.dex.nodes.InsnNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.dex.regions.Region
import jadx.plugins.script.runtime.JadxScriptInstance
class Stages(private val jadx: JadxScriptInstance) {
fun rawInsns(block: (MethodNode, Array<InsnNode?>) -> Unit) {
jadx.addPass(object : ScriptOrderedDecompilePass(
jadx,
"StageRawInsns",
runAfter = listOf("start")
) {
override fun visit(mth: IMethodNode) {
val mthNode = mth as MethodNode
mthNode.instructions?.let {
block.invoke(mthNode, it)
}
}
})
}
fun mthEarlyBlocks(block: (MethodNode, List<BlockNode>) -> Unit) {
mthBlocks(beforePass = "SSATransform", block)
}
fun mthBlocks(
beforePass: String = "RegionMakerVisitor",
block: (MethodNode, List<BlockNode>) -> Unit
) {
jadx.addPass(object : ScriptOrderedDecompilePass(
jadx,
"StageMthBlocks",
runBefore = listOf(beforePass)
) {
override fun visit(mth: IMethodNode) {
val mthNode = mth as MethodNode
mthNode.basicBlocks?.let {
block.invoke(mthNode, it)
}
}
})
}
fun mthRegions(block: (MethodNode, Region) -> Unit) {
jadx.addPass(object : ScriptOrderedDecompilePass(
jadx,
"StageMthRegions",
runBefore = listOf("PrepareForCodeGen")
) {
override fun visit(mth: IMethodNode) {
val mthNode = mth as MethodNode
mthNode.region?.let {
block.invoke(mthNode, it)
}
}
})
}
}
@@ -0,0 +1,69 @@
package jadx.plugins.script.runtime.data
import jadx.api.core.nodes.IClassNode
import jadx.api.core.nodes.IMethodNode
import jadx.api.core.nodes.IRootNode
import jadx.api.plugins.pass.JadxPass
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo
import jadx.api.plugins.pass.impl.SimpleJadxPassInfo
import jadx.api.plugins.pass.types.JadxDecompilePass
import jadx.api.plugins.pass.types.JadxPreparePass
import jadx.plugins.script.runtime.JadxScriptInstance
private fun buildScriptName(jadx: JadxScriptInstance, name: String) = "JadxScript${name}(${jadx.scriptName})"
private fun buildSimplePassInfo(jadx: JadxScriptInstance, name: String) =
SimpleJadxPassInfo(buildScriptName(jadx, name))
abstract class ScriptPreparePass(
private val jadx: JadxScriptInstance, private val name: String
) : JadxPreparePass {
override fun getInfo() = buildSimplePassInfo(jadx, name)
}
abstract class ScriptDecompilePass(
private val jadx: JadxScriptInstance, private val name: String
) : JadxDecompilePass {
override fun getInfo() = buildSimplePassInfo(jadx, name)
override fun init(root: IRootNode) {
}
override fun visit(cls: IClassNode): Boolean {
return true
}
override fun visit(mth: IMethodNode) {
}
}
abstract class ScriptOrderedPass(
private val jadx: JadxScriptInstance,
private val name: String,
private val runAfter: List<String> = listOf(),
private val runBefore: List<String> = listOf()
) : JadxPass {
override fun getInfo(): OrderedJadxPassInfo {
val scriptName = buildScriptName(jadx, name)
return OrderedJadxPassInfo(scriptName, scriptName, runAfter, runBefore)
}
}
abstract class ScriptOrderedPreparePass(
jadx: JadxScriptInstance, name: String, runAfter: List<String> = listOf(), runBefore: List<String> = listOf()
) : ScriptOrderedPass(jadx, name, runAfter, runBefore), JadxPreparePass {}
abstract class ScriptOrderedDecompilePass(
jadx: JadxScriptInstance, name: String, runAfter: List<String> = listOf(), runBefore: List<String> = listOf()
) : ScriptOrderedPass(jadx, name, runAfter, runBefore), JadxDecompilePass {
override fun init(root: IRootNode) {
}
override fun visit(cls: IClassNode): Boolean {
return true
}
override fun visit(mth: IMethodNode) {
}
}
@@ -0,0 +1,75 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package jadx.plugins.script.runtime
import jadx.api.JadxArgs
import jadx.api.JadxDecompiler
import jadx.api.JavaClass
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.pass.JadxPass
import jadx.plugins.script.runtime.data.*
import mu.KLogger
import mu.KotlinLogging
import java.io.File
open class JadxScriptBaseClass(private val scriptData: JadxScriptData) {
val scriptName = scriptData.scriptName
val log = KotlinLogging.logger("JadxScript:${scriptName}")
fun getJadxInstance() = JadxScriptInstance(scriptData, log)
fun println(message: Any?) {
log.info(message?.toString())
}
fun print(message: Any?) {
log.info(message?.toString())
}
}
class JadxScriptData(
val jadxInstance: JadxDecompiler,
val pluginContext: JadxPluginContext,
val scriptFile: File
) {
val afterLoad: MutableList<() -> Unit> = ArrayList()
val scriptName get() = scriptFile.name.removeSuffix(".jadx.kts")
}
class JadxScriptInstance(
private val scriptData: JadxScriptData,
val log: KLogger
) {
private val decompiler = scriptData.jadxInstance
val rename: RenamePass by lazy { RenamePass(this) }
val stages: Stages by lazy { Stages(this) }
val replace: Replace by lazy { Replace(this) }
val decompile: Decompile by lazy { Decompile(this) }
val search: Search by lazy { Search(this) }
val gui: Gui by lazy { Gui(this, scriptData.pluginContext.guiContext) }
val debug: Debug by lazy { Debug(this) }
val args: JadxArgs
get() = decompiler.args
val classes: List<JavaClass>
get() = decompiler.classes
val scriptFile get() = scriptData.scriptFile
val scriptName get() = scriptData.scriptName
fun afterLoad(block: () -> Unit) {
scriptData.afterLoad.add(block)
}
fun addPass(pass: JadxPass) {
scriptData.pluginContext.passContext.addPass(pass)
}
val internalDecompiler: JadxDecompiler
get() = decompiler
}
@@ -0,0 +1,46 @@
package jadx.plugins.script.runtime
import kotlinx.coroutines.runBlocking
import kotlin.script.experimental.annotations.KotlinScript
import kotlin.script.experimental.api.*
import kotlin.script.experimental.dependencies.*
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
import kotlin.script.experimental.jvm.jvm
@KotlinScript(
fileExtension = "jadx.kts",
compilationConfiguration = JadxScriptConfiguration::class
)
abstract class JadxScript
object JadxScriptConfiguration : ScriptCompilationConfiguration({
defaultImports(DependsOn::class, Repository::class)
jvm {
dependenciesFromCurrentContext(
wholeClasspath = true
)
}
baseClass(JadxScriptBaseClass::class)
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
})
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)
?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess()
return runBlocking { resolver.resolveFromScriptSourceAnnotations(annotations) }
.onSuccess {
context.compilationConfiguration.with {
dependencies.append(JvmDependency(it))
}.asSuccess()
}
}
@@ -0,0 +1,10 @@
/**
* Utils for use in scripts.
* Located in default package to reduce imports.
*/
import java.io.File
fun String.asFile(): File = File(this)
fun Int.hex(): String = Integer.toHexString(this)