chore: remove 'jadx-script-kotlin'
This commit is contained in:
@@ -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()
|
||||
}
|
||||
-37
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-111
@@ -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) })
|
||||
}
|
||||
}
|
||||
-212
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
-82
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
-92
@@ -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()
|
||||
}
|
||||
}
|
||||
-51
@@ -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")
|
||||
}
|
||||
}
|
||||
-44
@@ -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) }
|
||||
}
|
||||
}
|
||||
-30
@@ -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()
|
||||
}
|
||||
}
|
||||
-55
@@ -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
|
||||
}
|
||||
}
|
||||
-76
@@ -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()
|
||||
}
|
||||
}
|
||||
-137
@@ -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
|
||||
}
|
||||
}
|
||||
-58
@@ -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
|
||||
}
|
||||
}
|
||||
-26
@@ -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)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
-251
@@ -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()
|
||||
}
|
||||
}
|
||||
-108
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -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}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-29
@@ -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 }
|
||||
}
|
||||
}
|
||||
-79
@@ -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
|
||||
}
|
||||
-10
@@ -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)
|
||||
-34
@@ -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'" }
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-65
@@ -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!")
|
||||
}
|
||||
-117
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-52
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
-43
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-16
@@ -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)
|
||||
}
|
||||
}
|
||||
-76
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
-77
@@ -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
@@ -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}")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user