chore: remove 'jadx-script-kotlin'

This commit is contained in:
Skylot
2026-02-22 14:35:16 +00:00
parent b0954e9620
commit 859d479569
55 changed files with 0 additions and 2981 deletions
-18
View File
@@ -1,18 +0,0 @@
## JADX scripting support
### Examples
Check script examples in [`examples/`](https://github.com/skylot/jadx/tree/master/jadx-plugins/jadx-script-kotlin/examples/)(start with [`hello`](https://github.com/skylot/jadx/blob/master/jadx-plugins/jadx-script-kotlin/examples/hello.jadx.kts))
### Script usage
#### In jadx-cli
Just add script file as input
#### In jadx-gui
1. Add script file to the project (using `Add files` or `New script` by right-click menu on `Inputs/Scripts`)
2. Script will appear in `Inputs/Scripts` section
3. After script change, you can run it using `Run` button in script editor toolbar or reload whole project (`Reload` button in toolbar or `F5`).
Also, you can enable `Live reload` option in `File` menu to reload project automatically on scripts change
@@ -1,87 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("java-library")
kotlin("jvm")
}
version = System.getenv("JADX_SCRIPT_KOTLIN_PLUGIN_VERSION") ?: "dev"
dependencies {
compileOnly(project(":jadx-core"))
compileOnly(project(":jadx-commons:jadx-app-commons"))
compileOnly(project(":jadx-gui"))
implementation(kotlin("scripting-common"))
implementation(kotlin("scripting-jvm"))
implementation(kotlin("scripting-jvm-host"))
implementation(kotlin("scripting-ide-services"))
implementation(kotlin("scripting-compiler-embeddable"))
implementation(kotlin("compiler-embeddable"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
// allow to use maven dependencies in scripts
implementation(kotlin("scripting-dependencies"))
implementation(kotlin("scripting-dependencies-maven"))
// autocomplete support in editor
compileOnly("com.fifesoft:autocomplete:3.3.2")
compileOnly("com.fifesoft:rsyntaxtextarea:3.6.0")
// use KtLint for format and check jadx scripts
implementation("com.pinterest.ktlint:ktlint-rule-engine:1.8.0")
implementation("com.pinterest.ktlint:ktlint-ruleset-standard:1.8.0")
compileOnly("io.github.oshai:kotlin-logging-jvm:7.0.13")
compileOnly("org.slf4j:slf4j-api:2.0.17")
// register jadx script for IDE support (don't work now)
// kotlinScriptDef(project(":jadx-plugins:jadx-script-kotlin"))
testImplementation(project(":jadx-core"))
testRuntimeOnly(project(":jadx-plugins:jadx-dex-input"))
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
testImplementation("ch.qos.logback:logback-classic:1.5.22")
testImplementation("org.assertj:assertj-core:3.27.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
repositories {
mavenLocal()
mavenCentral()
google()
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
tasks {
register<Zip>("dist") {
group = "jadx-plugin"
dependsOn(jar)
from(jar)
from(project.configurations.runtimeClasspath)
archiveBaseName = project.name
destinationDirectory = layout.buildDirectory.dir("dist")
}
withType(Test::class) {
useJUnitPlatform()
}
}
@@ -1,34 +0,0 @@
import jadx.api.plugins.options.OptionFlag.PER_PROJECT
/**
* Custom resources regexp deobfuscator
*/
val jadx = getJadxInstance()
val regexOpt = jadx.options.registerString(
name = "regex",
desc = "Apply resources rename for file names matches regex",
defaultValue = """[Oo0]+\.xml""",
).flags(PER_PROJECT)
val regex = regexOpt.value.toRegex()
var n = 0
jadx.stages.prepare {
for (resFile in jadx.internalDecompiler.resources) {
val fullName = resFile.originalName
val name = fullName.substringAfterLast('/')
if (name matches regex) {
val path = fullName.substringBeforeLast('/') // TODO: path also may be obfuscated
val ext = name.substringAfterLast('.')
val newName = "$path/res-${n++}.$ext"
log.info { "renaming resource: '$fullName' to '$newName'" }
resFile.deobfName = newName
}
}
}
jadx.afterLoad {
log.info { "Renames count: $n" }
}
@@ -1,33 +0,0 @@
import jadx.api.plugins.options.OptionFlag.PER_PROJECT
/**
* Custom regexp deobfuscator
*/
val jadx = getJadxInstance()
jadx.args.isDeobfuscationOn = false
jadx.args.renameFlags = emptySet()
val regexOpt = jadx.options.registerString(
name = "regex",
desc = "Apply rename for names matches regex",
defaultValue = "[Oo0]+",
).flags(PER_PROJECT)
val regex = regexOpt.value.toRegex()
var n = 0
jadx.rename.all { name, node ->
when {
name matches regex -> {
val newName = "${node.typeName()}${n++}"
log.info { "renaming ${node.typeName()} '$node' to '$newName'" }
newName
}
else -> null
}
}
jadx.afterLoad {
log.info { "Renames count: $n" }
}
@@ -1,43 +0,0 @@
/**
* Animal deobfuscator ^_^
*/
@file:DependsOn("com.github.javafaker:javafaker:1.0.2")
import com.github.javafaker.Faker
import jadx.core.deobf.NameMapper
import java.util.Random
val jadx = getJadxInstance()
jadx.args.isDeobfuscationOn = false
jadx.args.renameFlags = emptySet()
val regex = """[Oo0]+""".toRegex()
val usedNames = mutableSetOf<String>()
val faker = Faker(Random(1))
var dups = 1
jadx.rename.all { name, node ->
when {
name matches regex -> {
val prefix = node.typeName().first()
val alias = faker.name().firstName().cap() + faker.animal().name().cap()
makeUnique(prefix, alias)
}
else -> null
}
}
fun makeUnique(prefix: Char, name: String): String {
while (true) {
val resName = prefix + NameMapper.removeInvalidCharsMiddle(name)
return if (usedNames.add(resName)) resName else "$resName${dups++}"
}
}
jadx.afterLoad {
log.info { "Renames count: ${usedNames.size + dups}, names: ${usedNames.size}, dups: $dups" }
}
fun String.cap() = this.replaceFirstChar(Char::uppercaseChar)
@@ -1,37 +0,0 @@
/**
* Rename method if specified string is found
*/
import jadx.api.plugins.input.insns.Opcode
import jadx.core.dex.nodes.MethodNode
val renamesMap = mapOf(
"specificString" to "newMethodName",
)
val jadx = getJadxInstance()
var n = 0
jadx.rename.all { _, node ->
var newName: String? = null
if (node is MethodNode) {
// use quick instructions scanner
node.codeReader?.visitInstructions { insn ->
if (insn.opcode == Opcode.CONST_STRING) {
insn.decode()
val constStr = insn.indexAsString
val renameStr = renamesMap[constStr]
if (renameStr != null) {
log.info { "Found '$constStr' in method $node, renaming to '$renameStr'" }
newName = renameStr
n++
}
}
}
}
newName
}
jadx.afterLoad {
log.info { "Script '$scriptName' renamed $n methods" }
}
@@ -1,89 +0,0 @@
/**
* Rename class and fields using strings from toString() method
*/
import jadx.core.deobf.NameMapper
import jadx.core.dex.attributes.AFlag
import jadx.core.dex.attributes.nodes.RenameReasonAttr
import jadx.core.dex.info.FieldInfo
import jadx.core.dex.instructions.ConstStringNode
import jadx.core.dex.instructions.IndexInsnNode
import jadx.core.dex.instructions.InsnType
import jadx.core.dex.instructions.args.InsnWrapArg
import jadx.core.dex.nodes.InsnNode
import jadx.core.dex.nodes.MethodNode
import jadx.plugins.script.kotlin.runtime.data.ScriptOrderedDecompilePass
val jadx = getJadxInstance()
// StringBuilder chain replaced by STR_CONCAT instruction in SimplifyVisitor
// Search for return with STR_CONCAT and process args
jadx.addPass(object : ScriptOrderedDecompilePass(
jadx,
"DeobfFromToString",
runAfter = listOf("SimplifyVisitor"),
) {
override fun visit(mth: MethodNode) {
if (mth.methodInfo.shortId == "toString()Ljava/lang/String;") {
val returnBlock = mth.exitBlock.predecessors.firstOrNull { it.contains(AFlag.RETURN) }
val lastInsn = returnBlock?.instructions?.lastOrNull()
if (lastInsn != null && lastInsn.type == InsnType.RETURN) {
val arg = lastInsn.getArg(0)
if (arg.isInsnWrap) {
val wrapInsn = (arg as InsnWrapArg).wrapInsn
if (wrapInsn.type == InsnType.STR_CONCAT) {
log.info { "Renaming using 'toString' in class: ${mth.parentClass}" }
processArgs(mth, wrapInsn)
}
}
}
}
}
val clsSepRgx = Regex("[ ({:]")
private fun processArgs(mth: MethodNode, wrapInsn: InsnNode): Boolean {
try {
var fldName: String? = null
for ((i, arg) in wrapInsn.arguments.withIndex()) {
val insn = arg.unwrap() ?: return false
if (i % 2 == 0) {
if (insn !is ConstStringNode) {
return false
}
var str = insn.string
if (i == 0) {
// class and first field name
val parts = str.split(clsSepRgx)
val clsName = parts[0]
if (NameMapper.isValidIdentifier(clsName)) {
mth.parentClass.run {
log.info { "rename class '$name' to '$clsName'" }
rename(clsName)
RenameReasonAttr.forNode(this).append("from toString()")
}
}
str = parts[1]
}
fldName = str.trim('\'', '=', ',', ' ', ':')
} else {
if (insn.type != InsnType.IGET) {
return false
}
val iget = insn as IndexInsnNode
val fldInfo = iget.index as FieldInfo
val fld = mth.parentClass.searchField(fldInfo)
if (fld != null && NameMapper.isValidIdentifier(fldName)) {
log.info { "rename field '${fld.name}' to '$fldName'" }
fld.rename(fldName)
RenameReasonAttr.forNode(fld).append("from toString()")
}
}
}
return true
} catch (e: Exception) {
log.error(e) { "Args process failed" }
return false
}
}
})
@@ -1,34 +0,0 @@
/**
* Rename method parameters from value in attached annotation
*/
import jadx.api.plugins.input.data.attributes.JadxAttrType
import jadx.core.deobf.NameMapper
import jadx.core.dex.nodes.MethodNode
import jadx.plugins.script.kotlin.runtime.data.ScriptDecompilePass
val annCls = "Lretrofit2/http/Query;"
val annParam = "value"
val jadx = getJadxInstance()
// access to method parameters variables available only in decompile passes
jadx.addPass(object : ScriptDecompilePass(jadx, "RenameParams") {
override fun visit(mth: MethodNode) {
// parameter annotations stored in method attribute
mth.get(JadxAttrType.ANNOTATION_MTH_PARAMETERS)?.let { paramsAttr ->
for ((paramNum, annAttr) in paramsAttr.paramList.withIndex()) {
val name = annAttr?.get(annCls)?.values?.get(annParam)?.value as String?
if (NameMapper.isValidIdentifier(name)) {
mth.argRegs[paramNum].name = name
log.info { "Rename param $paramNum to $name in method $mth" }
}
}
}
}
})
jadx.afterLoad {
// force decompilation and run rename pass for all classes (optional)
jadx.decompile.allThreaded()
}
@@ -1,41 +0,0 @@
import jadx.api.metadata.ICodeNodeRef
import jadx.core.dex.nodes.MethodNode
val jadx = getJadxInstance()
var savedBookmark: ICodeNodeRef? = null
jadx.gui.ifAvailable {
addPopupMenuAction(
"Set bookmark",
enabled = { true },
keyBinding = "B",
action = ::setBookmark,
)
addMenuAction(
"Jump to bookmark",
action = ::jumpToBookmark,
)
}
fun setBookmark(node: ICodeNodeRef) {
val enclosing = jadx.gui.enclosingNodeUnderCaret ?: run {
log.info { "No enclosing node" }
return
}
// You can bookmark a field, method or a class
val target = if (enclosing is MethodNode) enclosing else node
log.info { "Setting bookmark to: $target" }
savedBookmark = target
}
fun jumpToBookmark() {
savedBookmark?.let {
if (!jadx.gui.open(it)) {
log.warn { "Failed to jump to bookmark: $it" }
}
} ?: run {
log.info { "No bookmark" }
}
}
@@ -1,19 +0,0 @@
import jadx.api.metadata.ICodeNodeRef
val jadx = getJadxInstance()
jadx.gui.ifAvailable {
addPopupMenuAction(
"Print enclosing symbols under caret or mouse",
enabled = { true },
keyBinding = "G",
action = ::runAction,
)
}
fun runAction(node: ICodeNodeRef) {
log.info { "Node under caret: ${jadx.gui.nodeUnderCaret}" }
log.info { "Enclosing node under caret: ${jadx.gui.enclosingNodeUnderCaret}" }
log.info { "Node under mouse: ${jadx.gui.nodeUnderMouse}" }
log.info { "Enclosing Node under mouse: ${jadx.gui.enclosingNodeUnderMouse}" }
}
@@ -1,120 +0,0 @@
@file:DependsOn("org.apache.commons:commons-text:1.10.0")
import jadx.api.metadata.ICodeNodeRef
import jadx.core.codegen.TypeGen
import jadx.core.dex.instructions.args.ArgType
import jadx.core.dex.nodes.ClassNode
import jadx.core.dex.nodes.FieldNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.utils.exceptions.JadxRuntimeException
import org.apache.commons.text.StringEscapeUtils
val jadx = getJadxInstance()
jadx.gui.ifAvailable {
addPopupMenuAction(
"Custom Frida snippet (g)",
enabled = ::isActionEnabled,
keyBinding = "G",
action = ::runAction,
)
}
fun isActionEnabled(node: ICodeNodeRef): Boolean {
return node is MethodNode || node is ClassNode || node is FieldNode
}
fun runAction(node: ICodeNodeRef) {
try {
val fridaSnippet = generateFridaSnippet(node)
log.info { "Custom frida snippet:\n$fridaSnippet" }
jadx.gui.copyToClipboard(fridaSnippet)
} catch (e: Exception) {
log.error(e) { "Failed to generate Frida code snippet" }
}
}
fun generateFridaSnippet(node: ICodeNodeRef): String {
return when (node) {
is MethodNode -> generateMethodSnippet(node)
is ClassNode -> generateClassSnippet(node)
is FieldNode -> generateFieldSnippet(node)
else -> throw JadxRuntimeException("Unsupported node type: " + node.javaClass)
}
}
fun generateClassSnippet(cls: ClassNode): String {
return """let ${cls.name} = Java.use("${StringEscapeUtils.escapeEcmaScript(cls.rawName)}");"""
}
fun generateMethodSnippet(mthNode: MethodNode): String {
val methodInfo = mthNode.methodInfo
val methodName = if (methodInfo.isConstructor) {
"\$init"
} else {
StringEscapeUtils.escapeEcmaScript(methodInfo.name)
}
val overload = if (isOverloaded(mthNode)) {
".overload(${methodInfo.argumentsTypes.joinToString(transform = this::parseArgType)})"
} else {
""
}
val shortClassName = mthNode.parentClass.name
val argNames = mthNode.collectArgNodes().map { a -> a.name }
val args = argNames.joinToString(separator = ", ")
val logArgs = if (argNames.isNotEmpty()) {
argNames.joinToString(separator = " + ', ' + ", prefix = " + ', ' + ") { p -> "'$p: ' + $p" }
} else {
""
}
val clsSnippet = generateClassSnippet(mthNode.parentClass)
return if (methodInfo.isConstructor || methodInfo.returnType == ArgType.VOID) {
// no return value
"""
$clsSnippet
$shortClassName["$methodName"]$overload.implementation = function ($args) {
console.log('$shortClassName.$methodName is called'$logArgs);
this["$methodName"]($args);
};
""".trimIndent()
} else {
"""
$clsSnippet
$shortClassName["$methodName"]$overload.implementation = function ($args) {
console.log('$shortClassName.$methodName is called'$logArgs);
let ret = this["$methodName"]($args);
console.log('$shortClassName.$methodName return: ' + ret);
return ret;
};
""".trimIndent()
}
}
fun generateFieldSnippet(fld: FieldNode): String {
var rawFieldName = StringEscapeUtils.escapeEcmaScript(fld.name)
for (methodNode in fld.parentClass.methods) {
if (methodNode.name == rawFieldName) {
rawFieldName = "_$rawFieldName"
break
}
}
return """
${generateClassSnippet(fld.parentClass)}
${fld.name} = ${fld.parentClass.name}.$rawFieldName.value;
""".trimIndent()
}
fun isOverloaded(methodNode: MethodNode): Boolean {
return methodNode.parentClass.methods.stream().anyMatch { m: MethodNode ->
m.name == methodNode.name && methodNode.methodInfo.shortId != m.methodInfo.shortId
}
}
fun parseArgType(x: ArgType): String {
val typeStr = if (x.isArray) {
TypeGen.signature(x).replace("/", ".")
} else {
x.toString()
}
return "'$typeStr'"
}
@@ -1,23 +0,0 @@
/**
* Log events
*/
import jadx.api.plugins.events.JadxEvents
val jadx = getJadxInstance()
jadx.gui.ifAvailable {
// GUI only events
jadx.events.addListener(JadxEvents.NODE_RENAMED_BY_USER) { rename ->
log.info { "Rename from '${rename.oldName}' to '${rename.newName}' for node ${rename.node}" }
}
jadx.events.addListener(JadxEvents.RELOAD_PROJECT) {
log.info { "Project reloaded" }
}
jadx.events.addListener(JadxEvents.RELOAD_SETTINGS_WINDOW) {
log.info { "Settings window reloaded" }
}
}
@@ -1,11 +0,0 @@
/**
* Add menu action (into 'Plugins' section)
*/
val jadx = getJadxInstance()
jadx.gui.ifAvailable {
addMenuAction("Decompile All") {
jadx.decompile.allThreaded()
}
}
@@ -1,29 +0,0 @@
// logger is preferred for output
log.info { "Hello from jadx script!" }
// println will also work (will be redirected to logger)
println("println from script '$scriptName'")
// get jadx decompiler script instance
val jadx = getJadxInstance()
// adjust options if needed
jadx.args.isDeobfuscationOn = false
// change names
jadx.rename.all { name ->
when (name) {
"HelloWorld" -> "HelloJadx"
else -> null
}
}
// run some code after loading is finished
jadx.afterLoad {
log.info { "Loaded classes: ${jadx.classes.size}" }
// print first class code
jadx.classes.firstOrNull()?.let { cls ->
log.info { "Class: '${cls.name}'" }
log.info { cls.code }
}
}
@@ -1,30 +0,0 @@
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 ->
log.info { "Option: '${opt.name}', id: '${opt.id}', value: '${opt.value}'" }
}
}
@@ -1,22 +0,0 @@
/**
* Instructions modification example.
* Replace first arg with const string.
*/
import jadx.core.dex.instructions.ConstStringNode
import jadx.core.dex.instructions.InvokeNode
import jadx.core.dex.instructions.args.InsnArg
val jadx = getJadxInstance()
jadx.replace.insns { mth, insn ->
if (insn is InvokeNode) {
if (insn.callMth.shortId == "println(Ljava/lang/String;)V") {
val arg = insn.getArg(1)
val newArg = InsnArg.wrapInsnIntoArg(ConstStringNode("Jadx!"))
insn.setArg(1, newArg)
log.info { "Replace '$arg' with '$newArg' in $mth" }
}
}
null
}
@@ -1,59 +0,0 @@
/**
* Replace method call with calculated result.
* Useful for custom string deobfuscation.
*
* Example for sample from issue https://github.com/skylot/jadx/issues/1251
*/
import jadx.core.dex.instructions.ConstStringNode
import jadx.core.dex.instructions.InvokeNode
import jadx.core.dex.instructions.args.InsnArg
import jadx.core.dex.instructions.args.InsnWrapArg
import jadx.core.dex.instructions.args.RegisterArg
val jadx = getJadxInstance()
val mthSignature = "com.xshield.aa.iIiIiiiiII(Ljava/lang/String;)Ljava/lang/String;"
jadx.replace.insns { mth, insn ->
if (insn is InvokeNode && insn.callMth.rawFullId == mthSignature) {
val str = getConstStr(insn.getArg(0))
if (str != null) {
val resultStr = decode(str)
log.info { "Decode '$str' to '$resultStr' in $mth" }
return@insns ConstStringNode(resultStr)
}
}
null
}
fun getConstStr(arg: InsnArg): String? {
val insn = when (arg) {
is InsnWrapArg -> arg.wrapInsn
is RegisterArg -> arg.assignInsn
else -> null
}
if (insn is ConstStringNode) {
return insn.string
}
return null
}
/**
* Decompiled method, automatically converted to Kotlin by IntelliJ Idea
*/
fun decode(str: String): String {
val length = str.length
val cArr = CharArray(length)
var i = length - 1
while (i >= 0) {
val i2 = i - 1
cArr[i] = (str[i].code xor 'z'.code).toChar()
if (i2 < 0) {
break
}
i = i2 - 1
cArr[i2] = (str[i2].code xor '\u000c'.code).toChar()
}
return String(cArr)
}
@@ -1,64 +0,0 @@
// insert processing passes for different decompilation stages
import jadx.core.dex.instructions.InsnType
import jadx.core.dex.nodes.IRegion
import java.lang.Integer.max
val jadx = getJadxInstance()
// print raw instructions
jadx.stages.rawInsns { mth, insns ->
log.info { "Instructions for method: $mth" }
for ((offset, insn) in insns.withIndex()) {
insn?.let {
log.info { " 0x${offset.hex()}: $insn" }
}
}
}
// access method basic blocks
jadx.stages.mthBlocks { mth, blocks ->
// count invoke instructions
var invCount = 0
for (block in blocks) {
for (insn in block.instructions) {
if (insn.type == InsnType.INVOKE) {
invCount++
}
}
}
log.info { "Invokes count in method $mth = $invCount" }
}
// access method regions
jadx.stages.mthRegions { mth, region ->
// recursively count max depth of nested regions
fun countRegionsDepth(region: IRegion): Int {
val subBlocks = region.subBlocks
if (subBlocks.isEmpty()) {
return 0
}
var depth = 1
for (block in subBlocks) {
if (block is IRegion) {
depth = max(depth, 1 + countRegionsDepth(block))
}
}
return depth
}
val depth = countRegionsDepth(region)
log.info { "Max region depth in method $mth = $depth" }
if (depth > 5) {
jadx.debug.printMethodRegions(mth, printInsns = true)
}
}
jadx.afterLoad {
/*
Start full decompilation (optional):
1. jadx-cli start decompilation automatically
2. jadx-gui start decompilation only on class open or search, so you might need to force it
*/
// jadx.decompile.all()
}
@@ -1,37 +0,0 @@
package jadx.plugins.script.kotlin
import jadx.api.plugins.JadxPlugin
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.JadxPluginInfo
import jadx.api.plugins.JadxPluginInfoBuilder
import jadx.plugins.script.kotlin.gui.JadxScriptInputCategory
import jadx.plugins.script.kotlin.gui.JadxScriptOptionsUI
import jadx.plugins.script.kotlin.passes.JadxScriptAfterLoadPass
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
class JadxScriptKotlinPlugin : JadxPlugin {
companion object {
const val PLUGIN_ID = "jadx-script-kotlin"
}
override fun getPluginInfo(): JadxPluginInfo = JadxPluginInfoBuilder.pluginId(PLUGIN_ID)
.name("Jadx Script (Kotlin)")
.description("Scripting support for jadx using Kotlin script")
.homepage("https://github.com/jadx-decompiler/jadx-script-kotlin")
.requiredJadxVersion("1.5.4, r2596")
.provides("jadx-script") // conflict with bundled plugin from older jadx versions
.build()
override fun init(context: JadxPluginContext) {
val scriptOptions = JadxScriptAllOptions()
context.registerOptions(scriptOptions)
val scripts = ScriptEval().process(context, scriptOptions)
if (scripts.isNotEmpty()) {
context.addPass(JadxScriptAfterLoadPass(scripts))
context.guiContext?.let { guiContext ->
JadxScriptOptionsUI.setup(guiContext, scriptOptions)
JadxScriptInputCategory.register(context, guiContext)
}
}
}
}
@@ -1,111 +0,0 @@
package jadx.plugins.script.kotlin
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.api.plugins.JadxPluginContext
import jadx.core.utils.files.FileUtils
import java.nio.file.Path
import java.security.MessageDigest
import kotlin.io.path.exists
import kotlin.script.experimental.api.CompiledScript
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.SourceCode
import kotlin.script.experimental.jvm.CompiledJvmScriptsCache
import kotlin.script.experimental.jvm.impl.KJvmCompiledScript
import kotlin.script.experimental.jvmhost.loadScriptFromJar
import kotlin.script.experimental.jvmhost.saveToJar
private val log = KotlinLogging.logger {}
class ScriptCache {
private val enableCache = System.getProperty("JADX_SCRIPT_CACHE_ENABLE", "true").equals("true", ignoreCase = true)
fun build(context: JadxPluginContext): CompiledJvmScriptsCache {
if (!enableCache) {
return CompiledJvmScriptsCache.NoCache
}
val cacheDir = getCacheDir(context)
log.debug { "script cache created in : $cacheDir" }
return JadxScriptsCache(cacheDir)
}
/**
* Same as CompiledScriptJarsCache implementation,
* but remove all previous cache versions for the script with the same path and name.
* This should reduce old cache entries count
*/
class JadxScriptsCache(private val baseCacheDir: Path) : CompiledJvmScriptsCache {
override fun get(
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration,
): CompiledScript? {
val cacheDir = hashDir(baseCacheDir, script)
val file = hashFile(cacheDir, script, scriptCompilationConfiguration)
if (file.exists()) {
file.toFile().loadScriptFromJar().let {
log.debug { "loaded script from cache: $file" }
return it
}
}
log.debug { "script not found in cache: $file" }
FileUtils.deleteDirIfExists(cacheDir)
return null
}
override fun store(
compiledScript: CompiledScript,
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration,
) {
val jvmScript = (compiledScript as? KJvmCompiledScript)
?: throw IllegalArgumentException("Unsupported script type ${compiledScript::class.java.name}")
val cacheDir = hashDir(baseCacheDir, script)
val file = hashFile(cacheDir, script, scriptCompilationConfiguration)
FileUtils.deleteDirIfExists(cacheDir)
FileUtils.makeDirs(cacheDir)
jvmScript.saveToJar(file.toFile())
log.debug { "script cached: $file" }
}
}
private fun getCacheDir(context: JadxPluginContext): Path {
val cacheBaseDir = context.files().pluginCacheDir.resolve("compiled")
FileUtils.makeDirs(cacheBaseDir)
return cacheBaseDir
}
companion object {
private fun hashDir(baseCacheDir: Path, script: SourceCode): Path {
if (script.name == null && script.locationId == null) {
return baseCacheDir.resolve("tmp")
}
val digest = MessageDigest.getInstance("MD5")
digest.add(script.name)
digest.add(script.locationId)
return baseCacheDir.resolve(digest.digest().toHexString())
}
private fun hashFile(
cacheDir: Path,
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration,
): Path {
val digest = MessageDigest.getInstance("MD5")
digest.add(script.text)
scriptCompilationConfiguration.notTransientData.entries
.sortedBy { it.key.name }
.forEach {
digest.add(it.key.name)
digest.add(it.value.toString())
}
return cacheDir.resolve(digest.digest().toHexString() + ".jar")
}
private fun MessageDigest.add(str: String?) {
str?.let { this.update(it.toByteArray()) }
}
private fun ByteArray.toHexString(): String = joinToString("", transform = { "%02x".format(it) })
}
}
@@ -1,212 +0,0 @@
package jadx.plugins.script.kotlin
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.api.plugins.JadxPluginContext
import jadx.plugins.script.kotlin.runtime.JadxScriptData
import jadx.plugins.script.kotlin.runtime.JadxScriptTemplate
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.scripting.resolve.skipExtensionsResolutionForImplicitsExceptInnermost
import java.io.File
import kotlin.script.experimental.api.EvaluationResult
import kotlin.script.experimental.api.KotlinType
import kotlin.script.experimental.api.ResultValue
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptAcceptedLocation
import kotlin.script.experimental.api.ScriptCollectedData
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptConfigurationRefinementContext
import kotlin.script.experimental.api.ScriptDiagnostic.Severity
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
import kotlin.script.experimental.api.acceptedLocations
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.api.collectedAnnotations
import kotlin.script.experimental.api.compilationConfiguration
import kotlin.script.experimental.api.compilerOptions
import kotlin.script.experimental.api.constructorArgs
import kotlin.script.experimental.api.defaultIdentifier
import kotlin.script.experimental.api.defaultImports
import kotlin.script.experimental.api.displayName
import kotlin.script.experimental.api.fileExtension
import kotlin.script.experimental.api.filePathPattern
import kotlin.script.experimental.api.hostConfiguration
import kotlin.script.experimental.api.ide
import kotlin.script.experimental.api.implicitReceivers
import kotlin.script.experimental.api.isStandalone
import kotlin.script.experimental.api.onSuccess
import kotlin.script.experimental.api.refineConfiguration
import kotlin.script.experimental.api.with
import kotlin.script.experimental.dependencies.CompoundDependenciesResolver
import kotlin.script.experimental.dependencies.DependsOn
import kotlin.script.experimental.dependencies.FileSystemDependenciesResolver
import kotlin.script.experimental.dependencies.Repository
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
import kotlin.script.experimental.dependencies.resolveFromScriptSourceAnnotations
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.host.getScriptingClass
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.host.with
import kotlin.script.experimental.jvm.JvmGetScriptingClass
import kotlin.script.experimental.jvm.baseClassLoader
import kotlin.script.experimental.jvm.compilationCache
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
import kotlin.script.experimental.jvm.jvm
import kotlin.script.experimental.jvm.updateClasspath
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
import kotlin.system.measureTimeMillis
import kotlin.time.DurationUnit
import kotlin.time.toDuration
private val log = KotlinLogging.logger {}
object DefCompileConf : ScriptCompilationConfiguration(ScriptEval.buildDefaultCompileConf())
class ScriptEval {
companion object {
fun buildDefaultCompileConf(): ScriptCompilationConfiguration {
val scriptEval = ScriptEval()
val hostConf = scriptEval.buildHostConf(null)
return scriptEval.buildCompileConf(hostConf)
}
}
fun process(context: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List<JadxScriptData> {
val jadx = context.decompiler
val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") }
if (scripts.isEmpty()) {
return emptyList()
}
val scriptDataList = mutableListOf<JadxScriptData>()
for (scriptFile in scripts) {
val scriptData = JadxScriptData(jadx, context, scriptOptions, scriptFile)
scriptDataList.add(scriptData)
eval(context, scriptData)
}
return scriptDataList
}
private fun eval(
context: JadxPluginContext,
scriptData: JadxScriptData,
) {
scriptData.log.debug { "Loading script: ${scriptData.scriptFile.absolutePath}" }
val hostConf = buildHostConf(context)
val compileConf = buildCompileConf(hostConf)
val evalConf = buildEvalConf(scriptData, compileConf)
val scriptingHost = BasicJvmScriptingHost(hostConf)
val execTime = measureTimeMillis {
val result = scriptingHost.eval(scriptData.scriptFile.toScriptSource(), compileConf, evalConf)
processEvalResult(result, scriptData)
}
scriptData.log.debug { "Script '${scriptData.scriptName}' executed in ${execTime.toDuration(DurationUnit.MILLISECONDS)}" }
}
private fun processEvalResult(res: ResultWithDiagnostics<EvaluationResult>, scriptData: JadxScriptData) {
val log = scriptData.log
for (r in res.reports) {
val msg = r.render(withSeverity = false)
when (r.severity) {
Severity.FATAL, Severity.ERROR -> log.error(r.exception) { "Script execution error: $msg" }
Severity.WARNING -> log.warn { "Script execution issue: $msg" }
Severity.INFO -> log.info { "Script report: $msg" }
Severity.DEBUG -> log.debug { "Script debug: $msg" }
}
}
when (res) {
is ResultWithDiagnostics.Success -> {
when (val retVal = res.value.returnValue) {
is ResultValue.Error -> log.error(retVal.error) { "Script execution error:" }
is ResultValue.Value -> log.info { "Script execution result: $retVal" }
is ResultValue.Unit -> {}
ResultValue.NotEvaluated -> {}
}
}
is ResultWithDiagnostics.Failure -> {
scriptData.error = true
log.error { "Script execution failed: ${scriptData.scriptName}" }
}
}
}
fun buildHostConf(context: JadxPluginContext?) = ScriptingHostConfiguration {
jvm {
getScriptingClass(JvmGetScriptingClass())
baseClassLoader.put(JadxScriptTemplate::class.java.classLoader)
context?.let {
compilationCache(ScriptCache().build(context))
}
}
}
fun buildCompileConf(scriptingHostConf: ScriptingHostConfiguration) = ScriptCompilationConfiguration {
hostConfiguration.put(scriptingHostConf)
displayName.put("Jadx script")
defaultIdentifier.put("JadxScript")
fileExtension.put("jadx.kts")
filePathPattern.put(".*\\.jadx\\.kts")
val receiversTypes = listOf(KotlinType(JadxScriptTemplate::class))
implicitReceivers(receiversTypes)
skipExtensionsResolutionForImplicitsExceptInnermost(receiversTypes)
jvm {
dependenciesFromCurrentContext(
wholeClasspath = true,
)
}
addBaseClass<JadxScriptTemplate>()
defaultImports(DependsOn::class, Repository::class)
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
ide {
acceptedLocations(ScriptAcceptedLocation.Everywhere)
}
isStandalone(true)
// forcing compiler to not use modules while building script classpath
// because shadow jar remove all modules-info.class (https://github.com/GradleUp/shadow/issues/710)
compilerOptions.append("-Xjdk-release=1.8")
}
inline fun <reified T> ScriptCompilationConfiguration.Builder.addBaseClass() {
val kClass = T::class
defaultImports.append(kClass.java.name)
hostConfiguration.update {
it.with {
this[jvm.baseClassLoader] = kClass.java.classLoader
}
}
}
fun buildEvalConf(scriptData: JadxScriptData, compileConf: ScriptCompilationConfiguration): ScriptEvaluationConfiguration {
return ScriptEvaluationConfiguration {
hostConfiguration.put(compileConf[hostConfiguration]!!)
compilationConfiguration.put(compileConf)
constructorArgs(JadxScriptTemplate(scriptData))
}
}
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 { files: List<File> ->
log.debug { "add script dependency: $files" }
context.compilationConfiguration.with {
updateClasspath(files)
}.asSuccess()
}
}
}
@@ -1,82 +0,0 @@
package jadx.plugins.script.kotlin
import jadx.api.plugins.JadxPluginContext
import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.scripting.compiler.plugin.services.FirReplHistoryProviderImpl
import org.jetbrains.kotlin.scripting.compiler.plugin.services.firReplHistoryProvider
import org.jetbrains.kotlin.scripting.compiler.plugin.services.isReplSnippetSource
import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices
import kotlin.script.experimental.api.ReplAnalyzerResult
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.SourceCodeCompletionVariant
import kotlin.script.experimental.api.analysisDiagnostics
import kotlin.script.experimental.api.renderedResultType
import kotlin.script.experimental.api.repl
import kotlin.script.experimental.api.valueOrNull
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.host.with
import kotlin.script.experimental.jvm.util.isError
import kotlin.script.experimental.jvm.util.toSourceCodePosition
data class ScriptCompletionResult(
val completions: List<SourceCodeCompletionVariant>,
val reports: MutableList<ScriptDiagnostic>,
)
data class ScriptAnalyzeResult(
val success: Boolean,
val issues: List<ScriptDiagnostic>,
val renderType: String?,
)
class ScriptServices(pluginContext: JadxPluginContext? = null) {
companion object {
const val AUTO_COMPLETE_INSERT_STR = "ABCDEF" // defined at KJvmReplCompleter.INSERTED_STRING
}
private val compileConf: ScriptCompilationConfiguration
private val replCompiler: KJvmReplCompilerWithIdeServices
init {
val scriptEval = ScriptEval()
val hostConf = scriptEval.buildHostConf(pluginContext)
hostConf.with {
repl {
firReplHistoryProvider(FirReplHistoryProviderImpl())
isReplSnippetSource { sourceFile, _ ->
sourceFile?.name?.endsWith(".jadx.kts", ignoreCase = true) ?: false
}
}
}
compileConf = scriptEval.buildCompileConf(hostConf)
replCompiler = KJvmReplCompilerWithIdeServices(hostConf)
}
fun complete(scriptName: String, code: String, cursor: Int): ScriptCompletionResult {
val snippet = code.toScriptSource(scriptName)
val result = runBlocking {
replCompiler.complete(snippet, cursor.toSourceCodePosition(snippet), compileConf)
}
return ScriptCompletionResult(
completions = result.valueOrNull()?.toList() ?: emptyList(),
reports = result.reports.toMutableList(),
)
}
fun analyze(scriptName: String, code: String): ScriptAnalyzeResult {
val sourceCode = code.toScriptSource(scriptName)
val result = runBlocking {
replCompiler.analyze(sourceCode, 0.toSourceCodePosition(sourceCode), compileConf)
}
val analyzerResult = result.valueOrNull()
val issues = mutableListOf<ScriptDiagnostic>()
analyzerResult?.get(ReplAnalyzerResult.analysisDiagnostics)?.let(issues::addAll)
issues.addAll(result.reports)
return ScriptAnalyzeResult(
success = !result.isError(),
issues = issues,
renderType = analyzerResult?.get(ReplAnalyzerResult.renderedResultType),
)
}
}
@@ -1,92 +0,0 @@
package jadx.plugins.script.kotlin.gui
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.api.ICodeInfo
import jadx.api.impl.SimpleCodeInfo
import jadx.api.plugins.JadxPluginContext
import jadx.core.utils.exceptions.JadxRuntimeException
import jadx.core.utils.files.FileUtils
import jadx.gui.treemodel.JClass
import jadx.gui.treemodel.JEditableNode
import jadx.gui.ui.MainWindow
import jadx.gui.ui.panel.ContentPanel
import jadx.gui.ui.tab.TabbedPane
import jadx.gui.utils.NLS
import jadx.gui.utils.UiUtils
import jadx.gui.utils.ui.SimpleMenuItem
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
import java.nio.file.Path
import javax.swing.Icon
import javax.swing.ImageIcon
import javax.swing.JPopupMenu
private val log = KotlinLogging.logger {}
class JInputScript(
val pluginContext: JadxPluginContext,
private val scriptPath: Path,
) : JEditableNode() {
companion object {
private val SCRIPT_ICON: ImageIcon = UiUtils.openSvgIcon("nodes/kotlin_script")
}
private val name: String = scriptPath.fileName.toString().replace(".jadx.kts", "")
override fun hasContent(): Boolean {
return true
}
override fun getContentPanel(tabbedPane: TabbedPane): ContentPanel {
return ScriptContentPanel(pluginContext, tabbedPane, this)
}
override fun getCodeInfo(): ICodeInfo {
try {
return SimpleCodeInfo(FileUtils.readFile(scriptPath))
} catch (e: Exception) {
throw JadxRuntimeException("Failed to read script file: " + scriptPath.toAbsolutePath(), e)
}
}
override fun save(newContent: String?) {
try {
FileUtils.writeFile(scriptPath, newContent)
log.debug { "Script saved: ${scriptPath.toAbsolutePath()}" }
} catch (e: Exception) {
throw JadxRuntimeException("Failed to write script file: " + scriptPath.toAbsolutePath(), e)
}
}
override fun onTreePopupMenu(mainWindow: MainWindow): JPopupMenu {
val menu = JPopupMenu()
menu.add(SimpleMenuItem(NLS.str("popup.add_scripts")) { mainWindow.addFiles() })
menu.add(SimpleMenuItem(NLS.str("popup.new_script")) { mainWindow.addNewScript() })
menu.add(SimpleMenuItem(NLS.str("popup.remove")) { mainWindow.removeInput(scriptPath) })
menu.add(SimpleMenuItem(NLS.str("popup.rename")) { mainWindow.renameInput(scriptPath) })
return menu
}
override fun getSyntaxName(): String {
return SyntaxConstants.SYNTAX_STYLE_KOTLIN
}
override fun getJParent(): JClass? {
return null
}
override fun getIcon(): Icon {
return SCRIPT_ICON
}
override fun getName(): String {
return name
}
override fun makeString(): String {
return name
}
override fun getTooltip(): String {
return scriptPath.normalize().toAbsolutePath().toString()
}
}
@@ -1,51 +0,0 @@
package jadx.plugins.script.kotlin.gui
import jadx.api.plugins.JadxPluginContext
import jadx.gui.treemodel.JClass
import jadx.gui.treemodel.JNode
import jadx.gui.ui.MainWindow
import jadx.gui.utils.NLS
import jadx.gui.utils.UiUtils
import jadx.gui.utils.ui.SimpleMenuItem
import java.nio.file.Path
import javax.swing.Icon
import javax.swing.ImageIcon
import javax.swing.JPopupMenu
class JInputScripts(
pluginContext: JadxPluginContext,
scripts: List<Path>,
) : JNode() {
companion object {
private val INPUT_SCRIPTS_ICON: ImageIcon = UiUtils.openSvgIcon("nodes/scriptsModel")
}
init {
for (script in scripts) {
add(JInputScript(pluginContext, script))
}
}
override fun onTreePopupMenu(mainWindow: MainWindow): JPopupMenu {
val menu = JPopupMenu()
menu.add(SimpleMenuItem(NLS.str("popup.add_scripts")) { mainWindow.addFiles() })
menu.add(SimpleMenuItem(NLS.str("popup.new_script")) { mainWindow.addNewScript() })
return menu
}
override fun getJParent(): JClass? {
return null
}
override fun getIcon(): Icon {
return INPUT_SCRIPTS_ICON
}
override fun getID(): String {
return "JInputScripts"
}
override fun makeString(): String {
return NLS.str("tree.input_scripts")
}
}
@@ -1,44 +0,0 @@
package jadx.plugins.script.kotlin.gui
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.gui.JadxGuiContext
import jadx.gui.plugins.context.GuiPluginContext
import jadx.gui.plugins.context.ITreeInputCategory
import jadx.gui.settings.data.ITabStatePersist
import jadx.gui.treemodel.JNode
import java.nio.file.Path
object JadxScriptInputCategory {
fun register(pluginContext: JadxPluginContext, guiContext: JadxGuiContext) {
val internalContext = guiContext as GuiPluginContext
val inputCategory = InputScriptsBuilder(pluginContext)
internalContext.registerTreeInputCategory(inputCategory)
internalContext.registerTabStatePersistAdapter(InputScriptTabStatePersist(inputCategory))
}
}
class InputScriptsBuilder(private val pluginContext: JadxPluginContext) : ITreeInputCategory {
var scriptsRootNode: JInputScripts? = null
override fun filesFilter(file: Path): Boolean {
return file.fileName.toString().endsWith(".jadx.kts", ignoreCase = true)
}
override fun buildInputNode(files: List<Path>): JNode {
val scriptsNode = JInputScripts(pluginContext, files)
scriptsRootNode = scriptsNode
return scriptsNode
}
}
class InputScriptTabStatePersist(private val scriptsBuilder: InputScriptsBuilder) : ITabStatePersist {
override fun getNodeClass() = JInputScript::class.java
override fun save(node: JNode): String {
return node.name
}
override fun load(nodeName: String): JNode? {
return scriptsBuilder.scriptsRootNode?.searchNode { it.name.equals(nodeName) }
}
}
@@ -1,30 +0,0 @@
package jadx.plugins.script.kotlin.gui
import jadx.api.plugins.gui.ISettingsGroup
import jadx.api.plugins.gui.JadxGuiContext
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
import javax.swing.JPanel
object JadxScriptOptionsUI {
fun setup(guiContext: JadxGuiContext, scriptOptions: JadxScriptAllOptions) {
guiContext.settings().setCustomSettingsGroup(ScriptOptionsRootGroup(guiContext, scriptOptions))
}
}
private class ScriptOptionsRootGroup(
private val guiContext: JadxGuiContext,
private val scriptOptions: JadxScriptAllOptions,
) : ISettingsGroup {
override fun getTitle() = "Scripts"
override fun buildComponent() = JPanel() // empty panel for root node
override fun getSubGroups(): List<ISettingsGroup> {
val settings = guiContext.settings()
return scriptOptions.descriptions
.groupBy { it.script }
.map { (script, options) -> settings.buildSettingsGroupForOptions(script, options) }
.toList()
}
}
@@ -1,55 +0,0 @@
package jadx.plugins.script.kotlin.gui
import com.pinterest.ktlint.rule.engine.api.Code
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider
import org.ec4j.core.model.PropertyType
data class JadxLintError(
val line: Int,
val col: Int,
val ruleId: String,
val detail: String,
)
object KtLintUtils {
private val ktLint by lazy {
KtLintRuleEngine(
ruleProviders = StandardRuleSetProvider().getRuleProviders(),
editorConfigOverride = EditorConfigOverride.from(
CODE_STYLE_PROPERTY to CodeStyleValue.intellij_idea,
INDENT_STYLE_PROPERTY to PropertyType.IndentStyleValue.tab,
),
)
}
fun format(content: String): String {
val code = Code.fromSnippet(content, script = true)
return ktLint.format(
code,
rerunAfterAutocorrect = true,
defaultAutocorrect = true,
) { AutocorrectDecision.ALLOW_AUTOCORRECT }
}
fun lint(content: String): List<JadxLintError> {
val errors = mutableListOf<JadxLintError>()
val code = Code.fromSnippet(content, script = true)
ktLint.lint(code) { lintError ->
errors.add(
JadxLintError(
line = lintError.line,
col = lintError.col,
ruleId = lintError.ruleId.value,
detail = lintError.detail,
),
)
}
return errors
}
}
@@ -1,76 +0,0 @@
package jadx.plugins.script.kotlin.gui
import jadx.api.ICodeInfo
import jadx.gui.jobs.IBackgroundTask
import jadx.gui.jobs.LoadTask
import jadx.gui.settings.JadxSettings
import jadx.gui.ui.action.JadxAutoCompletion
import jadx.gui.ui.codearea.AbstractCodeArea
import jadx.gui.ui.panel.ContentPanel
import jadx.gui.utils.shortcut.ShortcutsController
import org.fife.ui.autocomplete.AutoCompletion
class ScriptCodeArea(contentPanel: ContentPanel, val scriptNode: JInputScript) :
AbstractCodeArea(contentPanel, scriptNode) {
private val autoCompletion: AutoCompletion
private val shortcutsController: ShortcutsController
init {
setSyntaxEditingStyle(scriptNode.syntaxName)
isCodeFoldingEnabled = true
closeCurlyBraces = true
shortcutsController = contentPanel.mainWindow.shortcutsController
val settings = contentPanel.mainWindow.settings
autoCompletion = addAutoComplete(settings)
}
private fun addAutoComplete(settings: JadxSettings): AutoCompletion {
val provider = ScriptCompleteProvider(this, scriptNode.pluginContext)
provider.setAutoActivationRules(false, ".")
val ac = JadxAutoCompletion(provider)
ac.setListCellRenderer(ScriptCompletionRenderer(settings))
ac.isAutoActivationEnabled = true
ac.autoCompleteSingleChoices = true
ac.install(this)
shortcutsController.bindImmediate(ac)
return ac
}
override fun getCodeInfo(): ICodeInfo {
return node.codeInfo
}
override fun getLoadTask(): IBackgroundTask {
return LoadTask(
{ node.codeInfo.getCodeStr() },
{ code ->
text = code
setCaretPosition(0)
setLoaded()
},
)
}
override fun refresh() {
text = node.codeInfo.getCodeStr()
}
fun updateCode(newCode: String?) {
val caretPos = caretPosition
text = newCode
setCaretPosition(caretPos)
scriptNode.isChanged = true
}
fun save() {
scriptNode.save(getText())
scriptNode.isChanged = false
}
override fun dispose() {
shortcutsController.unbindActionsForComponent(this)
autoCompletion.uninstall()
super.dispose()
}
}
@@ -1,137 +0,0 @@
package jadx.plugins.script.kotlin.gui
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.api.plugins.JadxPluginContext
import jadx.core.utils.exceptions.JadxRuntimeException
import jadx.gui.ui.codearea.AbstractCodeArea
import jadx.gui.utils.Icons
import jadx.plugins.script.kotlin.ScriptCompletionResult
import jadx.plugins.script.kotlin.ScriptServices
import jadx.plugins.script.kotlin.ScriptServices.Companion.AUTO_COMPLETE_INSERT_STR
import org.fife.ui.autocomplete.Completion
import org.fife.ui.autocomplete.CompletionProviderBase
import org.fife.ui.autocomplete.ParameterizedCompletion
import java.awt.Point
import javax.swing.Icon
import javax.swing.text.BadLocationException
import javax.swing.text.JTextComponent
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.SourceCodeCompletionVariant
private val log = KotlinLogging.logger {}
private val ICONS_MAP = mapOf<String, Icon>(
"class" to Icons.CLASS,
"method" to Icons.METHOD,
"field" to Icons.FIELD,
"property" to Icons.PROPERTY,
"parameter" to Icons.PARAMETER,
"package" to Icons.PACKAGE,
)
class ScriptCompleteProvider(
private val codeArea: AbstractCodeArea,
private val pluginContext: JadxPluginContext,
) : CompletionProviderBase() {
private val completions: List<Completion>
get() {
try {
val code = codeArea.getText()
val caretPos = codeArea.caretPosition
val scriptServices = ScriptServices(pluginContext)
val scriptName = codeArea.getNode().getName()
val result = scriptServices.complete(scriptName, code, caretPos)
if (result.completions.isEmpty()) {
return listOf()
}
val replacePos = getReplacePos(caretPos, result)
if (!result.reports.isEmpty()) {
log.debug { "Script completion reports: ${result.reports}" }
}
log.debug { "Completions:\n${result.completions.joinToString(separator = "\n")}" }
return convertCompletions(result.completions, code, replacePos)
} catch (e: Exception) {
log.error(e) { "Code completion failed" }
return listOf()
}
}
private fun convertCompletions(
completions: List<SourceCodeCompletionVariant>,
code: String,
replacePos: Int,
): List<Completion> {
val count = completions.size
val list = ArrayList<Completion>(count)
for (i in 0..<count) {
val c = completions[i]
if (c.icon == "keyword") {
// too many, not very useful
continue
}
val summary = if (c.icon == "method" && c.text != c.displayText) {
// add method args details for methods
"${c.displayText} ${c.tail}"
} else {
c.tail
}
list += ScriptCompletionData(
provider = this,
input = c.text,
code = code,
relevance = count - i,
replacePos = replacePos,
summary = summary,
toolTip = c.displayText,
icon = ICONS_MAP[c.icon] ?: Icons.FILE,
)
}
return list
}
@Throws(BadLocationException::class)
private fun getReplacePos(caretPos: Int, result: ScriptCompletionResult): Int {
val lineRaw = codeArea.getLineOfOffset(caretPos)
val lineStart = codeArea.getLineStartOffset(lineRaw)
val line = lineRaw + 1
val completeReport = result.reports.find { report ->
if (report.severity == ScriptDiagnostic.Severity.ERROR) {
report.location?.let { location ->
location.start.line == line && report.message.endsWith(AUTO_COMPLETE_INSERT_STR)
} ?: false
} else {
false
}
}
if (completeReport == null) {
log.warn { "Failed to find completion report in: ${result.reports}" }
return caretPos
}
result.reports.remove(completeReport)
val col = caretPos - lineStart + 1
return caretPos - (col - completeReport.location!!.start.col)
}
override fun getAlreadyEnteredText(comp: JTextComponent?): String? {
try {
val pos = codeArea.caretPosition
return codeArea.getText(0, pos)
} catch (e: Exception) {
throw JadxRuntimeException("Failed to get text before caret", e)
}
}
override fun getCompletionsAt(comp: JTextComponent, p: Point): List<Completion> {
return this.completions
}
override fun getCompletionsImpl(comp: JTextComponent): List<Completion> {
return this.completions
}
override fun getParameterizedCompletions(tc: JTextComponent): List<ParameterizedCompletion>? {
return null
}
}
@@ -1,58 +0,0 @@
package jadx.plugins.script.kotlin.gui
import org.fife.ui.autocomplete.Completion
import org.fife.ui.autocomplete.CompletionProvider
import javax.swing.Icon
import javax.swing.text.JTextComponent
class ScriptCompletionData(
private val provider: CompletionProvider,
private val relevance: Int,
private val input: String,
private val code: String,
private val replacePos: Int,
private val icon: Icon,
private val toolTip: String,
private val summary: String,
) : Completion {
override fun getInputText(): String {
return input
}
override fun getProvider(): CompletionProvider {
return provider
}
override fun getAlreadyEntered(comp: JTextComponent?): String? {
return provider.getAlreadyEnteredText(comp)
}
override fun getRelevance(): Int {
return relevance
}
override fun getReplacementText(): String {
return code.substring(0, replacePos) + input
}
override fun getIcon(): Icon {
return icon
}
override fun getSummary(): String {
return summary
}
override fun getToolTipText(): String {
return toolTip
}
override fun compareTo(other: Completion): Int {
return relevance.compareTo(other.relevance)
}
override fun toString(): String {
return input
}
}
@@ -1,26 +0,0 @@
package jadx.plugins.script.kotlin.gui
import jadx.gui.settings.JadxSettings
import jadx.gui.utils.UiUtils
import org.fife.ui.autocomplete.Completion
import org.fife.ui.autocomplete.CompletionCellRenderer
import javax.swing.JList
class ScriptCompletionRenderer(settings: JadxSettings) : CompletionCellRenderer() {
init {
displayFont = settings.codeFont
}
override fun prepareForOtherCompletion(
list: JList<*>?,
c: Completion?,
index: Int,
selected: Boolean,
hasFocus: Boolean,
) {
val cmpl = c as ScriptCompletionData
setText(
UiUtils.wrapHtml((UiUtils.escapeHtml(cmpl.inputText) + " " + UiUtils.fadeHtml(UiUtils.escapeHtml(cmpl.summary)))),
)
}
}
@@ -1,251 +0,0 @@
package jadx.plugins.script.kotlin.gui
import jadx.api.plugins.JadxPluginContext
import jadx.gui.logs.LogOptions
import jadx.gui.settings.LineNumbersMode
import jadx.gui.ui.action.ActionModel
import jadx.gui.ui.action.JadxGuiAction
import jadx.gui.ui.codearea.AbstractCodeArea
import jadx.gui.ui.codearea.AbstractCodeContentPanel
import jadx.gui.ui.codearea.SearchBar
import jadx.gui.ui.tab.TabbedPane
import jadx.gui.utils.Icons
import jadx.gui.utils.NLS
import jadx.gui.utils.UiUtils
import jadx.gui.utils.ui.NodeLabel
import jadx.plugins.script.kotlin.ScriptServices
import jadx.plugins.script.kotlin.runtime.JadxScriptData.Companion.JADX_SCRIPT_LOG_PREFIX
import org.fife.ui.rsyntaxtextarea.ErrorStrip
import org.fife.ui.rtextarea.RTextScrollPane
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.awt.BorderLayout
import java.awt.Component
import java.awt.Dimension
import java.awt.event.KeyEvent
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JButton
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.KeyStroke
import javax.swing.border.EmptyBorder
import kotlin.script.experimental.api.ScriptDiagnostic
class ScriptContentPanel(
private val pluginContext: JadxPluginContext,
panel: TabbedPane,
scriptNode: JInputScript,
) : AbstractCodeContentPanel(panel, scriptNode) {
private val scriptArea: ScriptCodeArea = ScriptCodeArea(this, scriptNode)
private val searchBar: SearchBar
private val codeScrollPane: RTextScrollPane
private val actionPanel: JPanel
private val resultLabel: JLabel = NodeLabel("")
private val errorService: ScriptErrorService = ScriptErrorService(scriptArea)
private val scriptLog: Logger = LoggerFactory.getLogger(JADX_SCRIPT_LOG_PREFIX + scriptNode.name)
init {
actionPanel = buildScriptActionsPanel()
searchBar = SearchBar(scriptArea)
codeScrollPane = RTextScrollPane(scriptArea)
initUI()
applySettings()
scriptArea.load()
}
private fun initUI() {
val topPanel = JPanel(BorderLayout())
topPanel.setBorder(EmptyBorder(5, 5, 5, 5))
topPanel.add(actionPanel, BorderLayout.NORTH)
topPanel.add(searchBar, BorderLayout.SOUTH)
val codePanel = JPanel(BorderLayout())
codePanel.setBorder(EmptyBorder(0, 0, 0, 0))
codePanel.add(codeScrollPane)
codePanel.add(ErrorStrip(scriptArea), BorderLayout.LINE_END)
setLayout(BorderLayout())
setBorder(EmptyBorder(0, 0, 0, 0))
add(topPanel, BorderLayout.NORTH)
add(codeScrollPane, BorderLayout.CENTER)
val key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton())
UiUtils.addKeyBinding(scriptArea, key, "SearchAction") { searchBar.toggle() }
}
private fun buildScriptActionsPanel(): JPanel {
val runAction = JadxGuiAction(ActionModel.SCRIPT_RUN, Runnable { this.runScript() })
val saveAction = JadxGuiAction(ActionModel.SCRIPT_SAVE, Runnable { scriptArea.save() })
runAction.shortcutComponent = scriptArea
saveAction.shortcutComponent = scriptArea
tabbedPane.mainWindow.shortcutsController.bindImmediate(runAction)
tabbedPane.mainWindow.shortcutsController.bindImmediate(saveAction)
val save = saveAction.makeButton()
scriptArea.scriptNode.addChangeListener { save.setEnabled(it) }
val check = JButton(NLS.str("script.check"), Icons.CHECK)
check.addActionListener { checkScript() }
val format = JButton(NLS.str("script.format"), Icons.FORMAT)
format.addActionListener { reformatCode() }
val scriptLog = JButton(NLS.str("script.log"), Icons.FORMAT)
scriptLog.addActionListener { showScriptLog() }
val panel = JPanel()
panel.setLayout(BoxLayout(panel, BoxLayout.LINE_AXIS))
panel.setBorder(EmptyBorder(0, 0, 0, 0))
panel.add(runAction.makeButton())
panel.add(Box.createRigidArea(Dimension(10, 0)))
panel.add(save)
panel.add(Box.createRigidArea(Dimension(10, 0)))
panel.add(check)
panel.add(Box.createRigidArea(Dimension(10, 0)))
panel.add(format)
panel.add(Box.createRigidArea(Dimension(30, 0)))
panel.add(resultLabel)
panel.add(Box.createHorizontalGlue())
panel.add(scriptLog)
return panel
}
private fun runScript() {
scriptArea.save()
if (!checkScript(runScript = true)) {
return
}
resetResultLabel()
val tabbedPane = getTabbedPane()
val mainWindow = tabbedPane.mainWindow
mainWindow.backgroundExecutor.execute(NLS.str("script.run"), {
try {
mainWindow.wrapper.reloadPasses()
} catch (e: Exception) {
scriptLog.error("Passes reload failed", e)
}
}, {
mainWindow.passesReloaded()
})
}
private fun checkScript(runScript: Boolean = false): Boolean {
try {
resetResultLabel()
val code = scriptArea.getText()
if (code.contains("@file:DependsOn")) {
if (!runScript) {
resultLabel.setText("Checks disabled for scripts with external dependencies")
}
return true
}
val fileName = scriptArea.getNode().getName()
val scriptServices = ScriptServices(pluginContext)
val result = scriptServices.analyze(fileName, code)
var success = result.success
val issues: List<ScriptDiagnostic> = result.issues
for (issue in issues) {
val severity = issue.severity
if (severity == ScriptDiagnostic.Severity.ERROR || severity == ScriptDiagnostic.Severity.FATAL) {
scriptLog.error(
issue.render(
withSeverity = false,
withLocation = true,
withException = true,
withStackTrace = true,
),
)
success = false
} else if (severity == ScriptDiagnostic.Severity.WARNING) {
scriptLog.warn("Compile issue: {}", issue)
}
}
val lintErrs: List<JadxLintError> = when {
success -> getLintIssues(code)
else -> listOf()
}
errorService.clearErrors()
errorService.addCompilerIssues(issues)
errorService.addLintErrors(lintErrs)
if (!success) {
resultLabel.setText("Compile issues: " + issues.size)
showScriptLog()
} else if (!lintErrs.isEmpty()) {
resultLabel.setText("Lint issues: " + lintErrs.size)
} else {
resultLabel.setText("OK")
}
errorService.apply()
return success
} catch (e: Throwable) {
scriptLog.error("Failed to check code", e)
return true
}
}
private fun getLintIssues(code: String): List<JadxLintError> {
try {
val lintErrs = KtLintUtils.lint(code)
for (error in lintErrs) {
scriptLog.warn("Lint issue: {} ({}:{})(ruleId={})", error.detail, error.line, error.col, error.ruleId)
}
return lintErrs
} catch (e: Throwable) { // can throw initialization error
scriptLog.warn("KtLint failed", e)
return listOf()
}
}
private fun reformatCode() {
resetResultLabel()
try {
val code = scriptArea.getText()
val formattedCode = KtLintUtils.format(code)
if (code != formattedCode) {
scriptArea.updateCode(formattedCode)
resultLabel.setText("Code updated")
errorService.clearErrors()
}
} catch (e: Throwable) { // can throw initialization error
scriptLog.error("Failed to reformat code", e)
}
}
private fun resetResultLabel() {
resultLabel.setText("")
}
private fun applySettings() {
val settings = getSettings()
codeScrollPane.setLineNumbersEnabled(settings.lineNumbersMode != LineNumbersMode.DISABLE)
codeScrollPane.gutter.setLineNumberFont(settings.codeFont)
scriptArea.loadSettings()
}
private fun showScriptLog() {
mainWindow.showLogViewer(LogOptions.forScript(getNode().getName()))
}
override fun getCodeArea(): AbstractCodeArea {
return scriptArea
}
override fun getChildrenComponent(): Component {
return codeArea
}
override fun loadSettings() {
applySettings()
updateUI()
}
override fun dispose() {
scriptArea.dispose()
}
}
@@ -1,108 +0,0 @@
package jadx.plugins.script.kotlin.gui
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument
import org.fife.ui.rsyntaxtextarea.parser.AbstractParser
import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult
import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice
import org.fife.ui.rsyntaxtextarea.parser.ParseResult
import org.fife.ui.rsyntaxtextarea.parser.ParserNotice
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.script.experimental.api.ScriptDiagnostic
class ScriptErrorService(private val scriptArea: ScriptCodeArea) : AbstractParser() {
private val result: DefaultParseResult = DefaultParseResult(this)
override fun parse(doc: RSyntaxDocument?, style: String?): ParseResult {
return result
}
fun clearErrors() {
result.clearNotices()
scriptArea.removeParser(this)
}
fun apply() {
scriptArea.removeParser(this)
scriptArea.addParser(this)
scriptArea.addNotify()
scriptArea.requestFocus()
jumpCaretToFirstError()
}
private fun jumpCaretToFirstError() {
val parserNotices = result.notices
if (parserNotices.isEmpty()) {
return
}
val notice = parserNotices.get(0)
var offset = notice.offset
if (offset == -1) {
try {
offset = scriptArea.getLineStartOffset(notice.line)
} catch (e: Exception) {
LOG.error("Failed to jump to first error", e)
return
}
}
scriptArea.scrollToPos(offset)
}
fun addCompilerIssues(issues: List<ScriptDiagnostic>) {
for (issue in issues) {
if (issue.severity == ScriptDiagnostic.Severity.DEBUG) {
continue
}
val notice: DefaultParserNotice?
val loc = issue.location
if (loc == null) {
notice = DefaultParserNotice(this, issue.message, 0)
} else {
try {
val line = loc.start.line
val offset = scriptArea.getLineStartOffset(line - 1) + loc.start.col
val len = if (loc.end == null) -1 else loc.end!!.col - loc.start.col
notice = DefaultParserNotice(this, issue.message, line, offset - 1, len)
notice.setLevel(convertLevel(issue.severity))
} catch (e: Exception) {
LOG.error("Failed to convert script issue", e)
continue
}
}
addNotice(notice)
}
}
fun addLintErrors(errors: List<JadxLintError>) {
for (error in errors) {
try {
val line = error.line
val offset = scriptArea.getLineStartOffset(line - 1) + error.col - 1
val word = scriptArea.getWordByPosition(offset)
val len = word?.length ?: -1
val notice = DefaultParserNotice(this, error.detail, line, offset, len)
notice.setLevel(ParserNotice.Level.WARNING)
addNotice(notice)
} catch (e: Exception) {
LOG.error("Failed to convert lint error", e)
}
}
}
private fun addNotice(notice: DefaultParserNotice) {
LOG.debug("Add notice: {}:{}:{} - {}", notice.line, notice.offset, notice.length, notice.message)
result.addNotice(notice)
}
companion object {
private val LOG: Logger = LoggerFactory.getLogger(ScriptErrorService::class.java)
private fun convertLevel(severity: ScriptDiagnostic.Severity): ParserNotice.Level {
return when (severity) {
ScriptDiagnostic.Severity.FATAL, ScriptDiagnostic.Severity.ERROR -> ParserNotice.Level.ERROR
ScriptDiagnostic.Severity.WARNING -> ParserNotice.Level.WARNING
ScriptDiagnostic.Severity.INFO, ScriptDiagnostic.Severity.DEBUG -> ParserNotice.Level.INFO
}
}
}
}
@@ -1,27 +0,0 @@
package jadx.plugins.script.kotlin.passes
import jadx.api.JadxDecompiler
import jadx.api.plugins.pass.impl.SimpleJadxPassInfo
import jadx.api.plugins.pass.types.JadxAfterLoadPass
import jadx.plugins.script.kotlin.runtime.JadxScriptData
class JadxScriptAfterLoadPass(private val scripts: List<JadxScriptData>) : JadxAfterLoadPass {
override fun getInfo() = SimpleJadxPassInfo("JadxScriptAfterLoad", "Execute scripts 'afterLoad' block")
override fun init(decompiler: JadxDecompiler) {
for (script in scripts) {
if (script.error) {
continue
}
try {
for (b in script.afterLoad) {
b.invoke()
}
} catch (e: Throwable) {
script.error = true
script.log.error(e) { "Error executing 'afterLoad' block in script: ${script.scriptFile.name}" }
}
}
}
}
@@ -1,29 +0,0 @@
package jadx.plugins.script.kotlin.runtime
import jadx.plugins.script.kotlin.DefCompileConf
import kotlin.script.experimental.annotations.KotlinScript
@KotlinScript(
displayName = "Jadx Script",
fileExtension = "jadx.kts",
filePathPattern = ".*\\.jadx\\.kts",
compilationConfiguration = DefCompileConf::class,
)
open class JadxScriptTemplate(
scriptData: JadxScriptData,
) {
val scriptName = scriptData.scriptName
val log = scriptData.log
private val scriptInstance = JadxScriptInstance(scriptData, log)
fun getJadxInstance() = scriptInstance
fun println(message: Any?) {
log.info { message }
}
fun print(message: Any?) {
log.info { message }
}
}
@@ -1,79 +0,0 @@
@file:JvmName("ScriptRuntime")
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package jadx.plugins.script.kotlin.runtime
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.api.JadxArgs
import jadx.api.JadxDecompiler
import jadx.api.JavaClass
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.events.IJadxEvents
import jadx.api.plugins.pass.JadxPass
import jadx.plugins.script.kotlin.runtime.data.Debug
import jadx.plugins.script.kotlin.runtime.data.Decompile
import jadx.plugins.script.kotlin.runtime.data.Gui
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
import jadx.plugins.script.kotlin.runtime.data.JadxScriptOptions
import jadx.plugins.script.kotlin.runtime.data.Rename
import jadx.plugins.script.kotlin.runtime.data.Replace
import jadx.plugins.script.kotlin.runtime.data.Search
import jadx.plugins.script.kotlin.runtime.data.Stages
import org.jetbrains.annotations.ApiStatus.Internal
import java.io.File
class JadxScriptData(
val jadxInstance: JadxDecompiler,
val pluginContext: JadxPluginContext,
val options: JadxScriptAllOptions,
val scriptFile: File,
) {
companion object {
const val JADX_SCRIPT_LOG_PREFIX = "JadxScript:"
}
val scriptName = scriptFile.name.removeSuffix(".jadx.kts")
val log = KotlinLogging.logger("$JADX_SCRIPT_LOG_PREFIX$scriptName")
val afterLoad = mutableListOf<() -> Unit>()
var error: Boolean = false
}
class JadxScriptInstance(
private val scriptData: JadxScriptData,
val log: KLogger,
) {
private val decompiler = scriptData.jadxInstance
val options: JadxScriptOptions by lazy { JadxScriptOptions(this, scriptData.options) }
val rename: Rename by lazy { Rename(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 events: IJadxEvents
get() = scriptData.pluginContext.events()
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.addPass(pass)
}
val internalDecompiler: JadxDecompiler
@Internal get() = decompiler
}
@@ -1,10 +0,0 @@
/**
* 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)
@@ -1,34 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
import jadx.core.dex.nodes.MethodNode
import jadx.core.dex.visitors.DotGraphVisitor
import jadx.core.utils.DebugUtils
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
import java.io.File
class Debug(private val jadx: JadxScriptInstance) {
fun printMethodRegions(mth: MethodNode, printInsns: Boolean = false) {
DebugUtils.printRegions(mth, printInsns)
}
fun saveCFG(mth: MethodNode, file: File = File("dump-mth-raw")) {
DotGraphVisitor.dumpRaw().save(file, mth)
}
fun printPreparePasses() {
jadx.internalDecompiler.root.preDecompilePasses.forEach { jadx.log.info { it.name } }
}
fun printPasses() {
jadx.internalDecompiler.root.passes.forEach { jadx.log.info { it.name } }
}
fun catchExceptions(label: String = "", code: () -> Unit) {
try {
code.invoke()
} catch (e: Throwable) {
jadx.log.error(e) { "Exception in '$label'" }
}
}
}
@@ -1,27 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
import jadx.api.JadxArgs
import jadx.api.JavaClass
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
import java.util.concurrent.Executors
class Decompile(private val jadx: JadxScriptInstance) {
fun all(ignoreCache: Boolean = false) {
if (ignoreCache) {
jadx.classes.forEach(JavaClass::reload)
} else {
jadx.classes.forEach(JavaClass::decompile)
}
}
fun allThreaded(threadsCount: Int = JadxArgs.DEFAULT_THREADS_COUNT) {
val executor = Executors.newFixedThreadPool(threadsCount)
val batches = jadx.internalDecompiler.decompileScheduler.buildBatches(jadx.classes)
for (batch in batches) {
executor.submit {
batch.forEach(JavaClass::decompile)
}
}
}
}
@@ -1,65 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
import jadx.api.metadata.ICodeNodeRef
import jadx.api.plugins.gui.JadxGuiContext
import jadx.plugins.script.kotlin.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) {
context().uiRun(block)
}
fun addMenuAction(name: String, action: () -> Unit) {
context().addMenuAction(name, action)
}
fun addPopupMenuAction(
name: String,
enabled: (ICodeNodeRef) -> Boolean = { _ -> true },
keyBinding: String? = null,
action: (ICodeNodeRef) -> Unit,
) {
context().addPopupMenuAction(name, enabled, keyBinding, action)
}
fun registerGlobalKeyBinding(id: String, keyBinding: String, action: () -> Unit): Boolean {
return context().registerGlobalKeyBinding(id, keyBinding, action)
}
fun copyToClipboard(str: String) {
context().copyToClipboard(str)
}
fun open(ref: ICodeNodeRef): Boolean = context().open(ref)
fun reloadActiveTab() = context().reloadActiveTab()
fun reloadAllTabs() = context().reloadAllTabs()
val nodeUnderCaret: ICodeNodeRef?
get() = context().nodeUnderCaret
val nodeUnderMouse: ICodeNodeRef?
get() = context().nodeUnderMouse
val enclosingNodeUnderCaret: ICodeNodeRef?
get() = context().enclosingNodeUnderCaret
val enclosingNodeUnderMouse: ICodeNodeRef?
get() = context().enclosingNodeUnderMouse
/**
* Save node rename in a project and run all needed UI updates
*/
fun applyNodeRename(node: ICodeNodeRef) = context().applyNodeRename(node)
private fun context(): JadxGuiContext =
guiContext ?: throw IllegalStateException("GUI plugins context not available!")
}
@@ -1,117 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
import jadx.api.plugins.options.JadxPluginOptions
import jadx.api.plugins.options.OptionDescription
import jadx.api.plugins.options.OptionFlag
import jadx.api.plugins.options.OptionType
import jadx.api.plugins.options.impl.JadxOptionDescription
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class JadxScriptAllOptions : JadxPluginOptions {
lateinit var values: Map<String, String>
val descriptions: MutableList<ScriptOptionDesc> = mutableListOf()
override fun setOptions(options: Map<String, String>) {
values = options
}
override fun getOptionsDescriptions(): List<OptionDescription> = descriptions
}
class ScriptOptionDesc(
val script: String,
optName: String,
desc: String,
defaultValue: String?,
values: List<String>,
type: OptionType,
) : JadxOptionDescription("jadx-script.$script.$optName", desc, defaultValue, values, type)
class ScriptOption<T>(
val name: String,
val id: String,
val optData: ScriptOptionDesc,
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
}
fun flags(vararg flags: OptionFlag): ScriptOption<T> {
optData.flags += flags
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 optData = ScriptOptionDesc(jadx.scriptName, name, desc, defaultValue, values, type)
options.descriptions.add(optData)
val optId = optData.name()
return ScriptOption(name, optId, optData) { convert.invoke(options.values[optId]) }
}
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()
}
}
}
}
@@ -1,52 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
import jadx.core.dex.attributes.AFlag
import jadx.core.dex.attributes.IAttributeNode
import jadx.core.dex.nodes.IDexNode
import jadx.core.dex.nodes.RootNode
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Rename(private val jadx: JadxScriptInstance) {
fun all(makeNewName: (String) -> String?) {
all { name, _ -> makeNewName.invoke(name) }
}
fun all(makeNewName: (String, IDexNode) -> String?) {
jadx.addPass(object : ScriptOrderedPreparePass(
jadx,
"RenameAll",
runBefore = listOf("RenameVisitor"),
) {
override fun init(root: RootNode) {
for (pkgNode in root.packages) {
rename(makeNewName, pkgNode, pkgNode.pkgInfo.name)
}
for (cls in root.classes) {
rename(makeNewName, cls, cls.name)
for (mth in cls.methods) {
if (!mth.isConstructor) {
rename(makeNewName, mth, mth.name)
}
}
for (fld in cls.fields) {
rename(makeNewName, fld, fld.name)
}
}
}
private inline fun <T : IDexNode> rename(
makeNewName: (String, IDexNode) -> String?,
node: T,
name: String,
) {
if (node is IAttributeNode && node.contains(AFlag.DONT_RENAME)) {
return
}
makeNewName.invoke(name, node)?.let {
node.rename(it)
}
}
})
}
}
@@ -1,43 +0,0 @@
package jadx.plugins.script.kotlin.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.kotlin.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))
}
}
}
}
}
@@ -1,16 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
import jadx.core.dex.nodes.ClassNode
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Search(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)
}
}
@@ -1,76 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
import jadx.core.dex.nodes.BlockNode
import jadx.core.dex.nodes.InsnNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.dex.nodes.RootNode
import jadx.core.dex.regions.Region
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Stages(private val jadx: JadxScriptInstance) {
fun prepare(block: (RootNode) -> Unit) {
jadx.addPass(object : ScriptPreparePass(jadx, "StagePrepare") {
override fun init(root: RootNode) {
jadx.debug.catchExceptions("Prepare init block") {
block.invoke(root)
}
}
})
}
fun rawInsns(block: (MethodNode, Array<InsnNode?>) -> Unit) {
jadx.addPass(object : ScriptOrderedDecompilePass(
jadx,
"StageRawInsns",
runAfter = listOf("start"),
) {
override fun visit(mth: MethodNode) {
mth.instructions?.let {
jadx.debug.catchExceptions("Method instructions visit") {
block.invoke(mth, 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: MethodNode) {
mth.basicBlocks?.let {
jadx.debug.catchExceptions("Method blocks visit") {
block.invoke(mth, it)
}
}
}
})
}
fun mthRegions(block: (MethodNode, Region) -> Unit) {
jadx.addPass(object : ScriptOrderedDecompilePass(
jadx,
"StageMthRegions",
runBefore = listOf("PrepareForCodeGen"),
) {
override fun visit(mth: MethodNode) {
mth.region?.let {
jadx.debug.catchExceptions("Method region visit") {
block.invoke(mth, it)
}
}
}
})
}
}
@@ -1,77 +0,0 @@
package jadx.plugins.script.kotlin.runtime.data
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.core.dex.nodes.ClassNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.dex.nodes.RootNode
import jadx.plugins.script.kotlin.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: RootNode) {
}
override fun visit(cls: ClassNode): Boolean {
return true
}
override fun visit(mth: MethodNode) {
}
}
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: RootNode) {
}
override fun visit(cls: ClassNode): Boolean {
return true
}
override fun visit(mth: MethodNode) {
}
}
@@ -1 +0,0 @@
jadx.plugins.script.kotlin.JadxScriptKotlinPlugin
@@ -1,51 +0,0 @@
package jadx.plugins.script
import jadx.api.JadxArgs
import jadx.api.JadxDecompiler
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.io.File
import kotlin.system.measureTimeMillis
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class JadxScriptPluginTest {
@BeforeAll
fun disableCache() {
System.setProperty("JADX_SCRIPT_CACHE_ENABLE", "false")
}
@AfterAll
fun clear() {
System.clearProperty("JADX_SCRIPT_CACHE_ENABLE")
}
@Test
fun integrationTest() {
val args = JadxArgs()
args.inputFiles.run {
add(getSampleFile("hello.smali"))
add(getSampleFile("test.jadx.kts"))
add(getSampleFile("test-deps.jadx.kts"))
}
val elapsed = measureTimeMillis {
JadxDecompiler(args).use { jadx ->
jadx.load()
assertThat(jadx.classes)
.hasSize(1)
.allMatch { it.name == "HelloJadx" }
}
}
println("Elapsed time: ${elapsed.toDuration(DurationUnit.MILLISECONDS)}")
}
private fun getSampleFile(file: String): File {
val resFile = javaClass.classLoader.getResource("samples/$file")
return File(resFile!!.toURI())
}
}
@@ -1,67 +0,0 @@
package jadx.plugins.script
import jadx.plugins.script.kotlin.ScriptServices
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import kotlin.script.experimental.api.ScriptDiagnostic.Severity.ERROR
class ScriptServicesTest {
@Test
fun testAnalyzeSimple() {
val name = "simple"
val script = getSampleScript(name)
val result = ScriptServices().analyze(name, script)
println(result)
assertThat(result.success).isTrue()
assertThat(result.issues).noneMatch { it.severity == ERROR }
}
@Disabled("External dependencies not resolved")
@Test
fun testAnalyzeDeps() {
val name = "test-deps"
val script = getSampleScript(name)
val result = ScriptServices().analyze(name, script)
println(result)
assertThat(result.success).isTrue()
assertThat(result.issues).noneMatch { it.severity == ERROR }
}
@Test
fun testComplete() {
val name = "simple"
val script = getSampleScript(name)
val idx = script.indexOf("jadx.log.info")
val completePos = idx + 7 // jadx.lo| <- complete 'log'
val curScript = script.substring(0, completePos)
val result = ScriptServices().complete(name, curScript, completePos)
println(result)
assertThat(result.completions)
.hasSize(1)
.allMatch { c -> c.text == "log" }
}
@Disabled("External dependencies not resolved")
@Test
fun testCompleteDeps() {
val sampleName = "test-deps"
val script = getSampleScript(sampleName)
val startPos = script.indexOf("StringEscapeUtils.escapeJava")
val completePos = startPos + 26 // StringEscapeUtils.escapeJa| <- complete 'escapeJava('
val exprEnd = script.indexOf('}', startIndex = completePos)
val curScript = script.removeRange(completePos, exprEnd)
val result = ScriptServices().complete(sampleName, curScript, completePos)
println(result)
assertThat(result.completions)
.hasSize(1)
.allMatch { c -> c.text == "escapeJava(" }
}
private fun getSampleScript(scriptName: String): String {
val resFile = javaClass.classLoader.getResource("samples/$scriptName.jadx.kts")
return resFile!!.readText()
}
}
@@ -1,10 +0,0 @@
.class LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 2
sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v0, "Hello, World"
invoke-virtual {p0, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
@@ -1,5 +0,0 @@
val jadx = getJadxInstance()
jadx.afterLoad {
jadx.log.info { "Hello" }
}
@@ -1,11 +0,0 @@
@file:DependsOn("org.apache.commons:commons-text:1.10.0")
import org.apache.commons.text.StringEscapeUtils
val jadx = getJadxInstance()
jadx.afterLoad {
jadx.classes.forEach {
jadx.log.info { "Escaped name: ${StringEscapeUtils.escapeJava(it.fullName)}" }
}
}
@@ -1,20 +0,0 @@
import jadx.api.CommentsLevel
val jadx = getJadxInstance()
jadx.args.commentsLevel = CommentsLevel.NONE
jadx.args.isDeobfuscationOn = false
jadx.args.renameFlags = emptySet()
jadx.rename.all { name ->
when (name) {
"HelloWorld" -> "HelloJadx"
else -> null
}
}
jadx.afterLoad {
println("Loaded classes: ${jadx.classes.size}")
jadx.classes.forEach {
println("Class '${it.name}':\n${it.code}")
}
}
-1
View File
@@ -26,7 +26,6 @@ include("jadx-plugins:jadx-java-convert")
include("jadx-plugins:jadx-rename-mappings")
include("jadx-plugins:jadx-kotlin-metadata")
include("jadx-plugins:jadx-kotlin-source-debug-extension")
include("jadx-plugins:jadx-script-kotlin")
include("jadx-plugins:jadx-xapk-input")
include("jadx-plugins:jadx-aab-input")
include("jadx-plugins:jadx-apkm-input")