feat(script): add code area popup menu action

This commit is contained in:
Skylot
2022-11-04 21:49:18 +00:00
parent a2f018a00b
commit acbe94df27
19 changed files with 385 additions and 22 deletions
@@ -1,5 +1,14 @@
package jadx.api.plugins.gui;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.KeyStroke;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeNodeRef;
public interface JadxGuiContext {
/**
@@ -7,5 +16,32 @@ public interface JadxGuiContext {
*/
void uiRun(Runnable runnable);
/**
* Add global menu entry ('Plugins' section)
*/
void addMenuAction(String name, Runnable action);
/**
* Add code viewer popup menu entry
*
* @param name entry title
* @param enabled check if entry should be enabled, called on popup creation
* @param keyBinding optional assigned keybinding {@link KeyStroke#getKeyStroke(String)}
*/
void addPopupMenuAction(String name,
@Nullable Function<ICodeNodeRef, Boolean> enabled,
@Nullable String keyBinding,
Consumer<ICodeNodeRef> action);
/**
* Attach new key binding to main window
*
* @param id unique ID string
* @param keyBinding keybinding string {@link KeyStroke#getKeyStroke(String)}
* @param action runnable action
* @return false if already registered
*/
boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action);
void copyToClipboard(String str);
}
@@ -93,7 +93,7 @@ public class JadxWrapper {
decompiler = null;
}
if (guiPluginsContext != null) {
guiPluginsContext.reset();
resetGuiPluginsContext();
guiPluginsContext = null;
}
}
@@ -142,6 +142,14 @@ public class JadxWrapper {
decompiler.getPluginsContext().setGuiContext(guiPluginsContext);
}
public GuiPluginsContext getGuiPluginsContext() {
return guiPluginsContext;
}
public void resetGuiPluginsContext() {
guiPluginsContext.reset();
}
/**
* Get the complete list of classes
*/
@@ -0,0 +1,65 @@
package jadx.gui.plugins.context;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.KeyStroke;
import org.jetbrains.annotations.Nullable;
import jadx.api.metadata.ICodeNodeRef;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.JNodeAction;
public class CodePopupAction {
private final String name;
private final Function<ICodeNodeRef, Boolean> enabledCheck;
private final String keyBinding;
private final Consumer<ICodeNodeRef> action;
public CodePopupAction(String name, Function<ICodeNodeRef, Boolean> enabled, String keyBinding, Consumer<ICodeNodeRef> action) {
this.name = name;
this.enabledCheck = enabled;
this.keyBinding = keyBinding;
this.action = action;
}
public JNodeAction buildAction(CodeArea codeArea) {
return new NodeAction(this, codeArea);
}
private static class NodeAction extends JNodeAction {
private final CodePopupAction data;
public NodeAction(CodePopupAction data, CodeArea codeArea) {
super(data.name, codeArea);
if (data.keyBinding != null) {
KeyStroke key = KeyStroke.getKeyStroke(data.keyBinding);
if (key == null) {
throw new IllegalArgumentException("Failed to parse key stroke: " + data.keyBinding);
}
addKeyBinding(key, data.name);
}
this.data = data;
}
@Override
public boolean isActionEnabled(@Nullable JNode node) {
if (node == null) {
return false;
}
ICodeNodeRef codeNode = node.getCodeNodeRef();
if (codeNode == null) {
return false;
}
return data.enabledCheck.apply(codeNode);
}
@Override
public void runAction(JNode node) {
Runnable r = () -> data.action.accept(node.getCodeNodeRef());
getCodeArea().getMainWindow().getBackgroundExecutor().execute(data.name, r);
}
}
}
@@ -1,12 +1,23 @@
package jadx.gui.plugins.context;
import javax.swing.JMenu;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.plugins.gui.JadxGuiContext;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.JNodePopupBuilder;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.ActionHandler;
@@ -15,11 +26,14 @@ public class GuiPluginsContext implements JadxGuiContext {
private final MainWindow mainWindow;
private final List<CodePopupAction> codePopupActionList = new ArrayList<>();
public GuiPluginsContext(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
public void reset() {
codePopupActionList.clear();
JMenu pluginsMenu = mainWindow.getPluginsMenu();
pluginsMenu.removeAll();
pluginsMenu.setVisible(false);
@@ -44,4 +58,40 @@ public class GuiPluginsContext implements JadxGuiContext {
pluginsMenu.add(item);
pluginsMenu.setVisible(true);
}
@Override
public void addPopupMenuAction(String name, @Nullable Function<ICodeNodeRef, Boolean> enabled,
@Nullable String keyBinding, Consumer<ICodeNodeRef> action) {
codePopupActionList.add(new CodePopupAction(name, enabled, keyBinding, action));
}
public void appendPopupMenus(CodeArea codeArea, JNodePopupBuilder popup) {
if (codePopupActionList.isEmpty()) {
return;
}
popup.addSeparator();
for (CodePopupAction codePopupAction : codePopupActionList) {
popup.add(codePopupAction.buildAction(codeArea));
}
}
@Override
public boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action) {
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyBinding);
if (keyStroke == null) {
throw new IllegalArgumentException("Failed to parse key binding: " + keyBinding);
}
JPanel mainPanel = (JPanel) mainWindow.getContentPane();
Object prevBinding = mainPanel.getInputMap().get(keyStroke);
if (prevBinding != null) {
return false;
}
UiUtils.addKeyBinding(mainPanel, keyStroke, id, action);
return true;
}
@Override
public void copyToClipboard(String str) {
UiUtils.copyToClipboard(str);
}
}
@@ -2,7 +2,10 @@ package jadx.gui.plugins.script
import com.pinterest.ktlint.core.KtLint
import com.pinterest.ktlint.core.LintError
import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties.indentStyleProperty
import com.pinterest.ktlint.core.api.EditorConfigOverride
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider
import org.ec4j.core.model.PropertyType
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@@ -12,11 +15,18 @@ object KtLintUtils {
val rules = lazy { StandardRuleSetProvider().getRuleProviders() }
val configOverride = lazy {
EditorConfigOverride.from(
indentStyleProperty to PropertyType.IndentStyleValue.tab
)
}
fun format(code: String, fileName: String): String {
val params = KtLint.ExperimentalParams(
text = code,
fileName = fileName,
ruleProviders = rules.value,
editorConfigOverride = configOverride.value,
script = true,
cb = { e: LintError, corrected ->
if (!corrected) {
@@ -33,6 +43,7 @@ object KtLintUtils {
text = code,
fileName = fileName,
ruleProviders = rules.value,
editorConfigOverride = configOverride.value,
script = true,
cb = { e: LintError, corrected ->
if (!corrected) {
@@ -63,7 +63,9 @@ public class ScriptCodeArea extends AbstractCodeArea {
}
public void updateCode(String newCode) {
int caretPos = getCaretPosition();
setText(newCode);
setCaretPosition(caretPos);
scriptNode.setChanged(true);
}
@@ -23,6 +23,7 @@ import com.pinterest.ktlint.core.LintError;
import kotlin.script.experimental.api.ScriptDiagnostic;
import jadx.gui.JadxWrapper;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.treemodel.JInputScript;
@@ -131,7 +132,9 @@ public class ScriptContentPanel extends AbstractCodeContentPanel {
MainWindow mainWindow = tabbedPane.getMainWindow();
mainWindow.getBackgroundExecutor().execute(NLS.str("script.run"), () -> {
try {
mainWindow.getWrapper().getDecompiler().reloadPasses();
JadxWrapper wrapper = mainWindow.getWrapper();
wrapper.resetGuiPluginsContext();
wrapper.getDecompiler().reloadPasses();
} catch (Exception e) {
LOG.error("Passes reload failed", e);
}
@@ -149,23 +152,24 @@ public class ScriptContentPanel extends AbstractCodeContentPanel {
ScriptCompiler scriptCompiler = new ScriptCompiler(fileName);
ScriptAnalyzeResult result = scriptCompiler.analyze(code, scriptArea.getCaretPosition());
List<ScriptDiagnostic> errors = result.getErrors();
for (ScriptDiagnostic error : errors) {
LOG.warn("Parse error: {}", error);
List<ScriptDiagnostic> issues = result.getIssues();
for (ScriptDiagnostic issue : issues) {
LOG.warn("Compiler issue: {}", issue);
}
boolean success = issues.stream().map(ScriptDiagnostic::getSeverity)
.noneMatch(s -> s == ScriptDiagnostic.Severity.ERROR || s == ScriptDiagnostic.Severity.FATAL);
List<LintError> lintErrs = Collections.emptyList();
if (errors.isEmpty()) {
if (success) {
lintErrs = getLintIssues(code, fileName);
}
errorService.clearErrors();
errorService.addErrors(errors);
errorService.addCompilerIssues(issues);
errorService.addLintErrors(lintErrs);
errorService.apply();
boolean success = errors.isEmpty();
if (!success) {
resultLabel.setText("Parsing errors: " + errors.size());
resultLabel.setText("Compiler issues: " + issues.size());
} else if (!lintErrs.isEmpty()) {
resultLabel.setText("Lint issues: " + lintErrs.size());
}
@@ -62,20 +62,21 @@ public class ScriptErrorService extends AbstractParser {
}
}
public void addErrors(List<ScriptDiagnostic> errors) {
for (ScriptDiagnostic error : errors) {
public void addCompilerIssues(List<ScriptDiagnostic> issues) {
for (ScriptDiagnostic issue : issues) {
DefaultParserNotice notice;
SourceCode.Location loc = error.getLocation();
SourceCode.Location loc = issue.getLocation();
if (loc == null) {
notice = new DefaultParserNotice(this, error.getMessage(), 0);
notice = new DefaultParserNotice(this, issue.getMessage(), 0);
} else {
try {
int line = loc.getStart().getLine();
int offset = scriptArea.getLineStartOffset(line - 1) + loc.getStart().getCol();
int len = loc.getEnd() == null ? -1 : loc.getEnd().getCol() - loc.getStart().getCol();
notice = new DefaultParserNotice(this, error.getMessage(), line, offset - 1, len);
notice = new DefaultParserNotice(this, issue.getMessage(), line, offset - 1, len);
notice.setLevel(convertLevel(issue.getSeverity()));
} catch (Exception e) {
LOG.error("Failed to convert script error", e);
LOG.error("Failed to convert script issue", e);
continue;
}
}
@@ -83,6 +84,20 @@ public class ScriptErrorService extends AbstractParser {
}
}
private static ParserNotice.Level convertLevel(ScriptDiagnostic.Severity severity) {
switch (severity) {
case FATAL:
case ERROR:
return ParserNotice.Level.ERROR;
case WARNING:
return ParserNotice.Level.WARNING;
case INFO:
case DEBUG:
return ParserNotice.Level.INFO;
}
return ParserNotice.Level.ERROR;
}
public void addLintErrors(List<LintError> errors) {
for (LintError error : errors) {
try {
@@ -21,6 +21,7 @@ import jadx.api.data.impl.JadxNodeRef;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.nodes.ICodeNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.ClassCodeContentPanel;
@@ -164,6 +165,11 @@ public class JClass extends JLoadableNode implements JRenameNode {
return cls;
}
@Override
public ICodeNode getCodeNodeRef() {
return cls.getClassNode();
}
@Override
public JClass getJParent() {
return jParent;
@@ -16,6 +16,7 @@ import jadx.api.JavaNode;
import jadx.api.data.ICodeRename;
import jadx.api.data.impl.JadxCodeRename;
import jadx.api.data.impl.JadxNodeRef;
import jadx.api.metadata.ICodeNodeRef;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
@@ -47,6 +48,11 @@ public class JField extends JNode implements JRenameNode {
return field;
}
@Override
public ICodeNodeRef getCodeNodeRef() {
return field.getFieldNode();
}
@Override
public JClass getJParent() {
return jParent;
@@ -17,6 +17,7 @@ import jadx.api.JavaNode;
import jadx.api.data.ICodeRename;
import jadx.api.data.impl.JadxCodeRename;
import jadx.api.data.impl.JadxNodeRef;
import jadx.api.metadata.ICodeNodeRef;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
@@ -53,6 +54,11 @@ public class JMethod extends JNode implements JRenameNode {
return mth;
}
@Override
public ICodeNodeRef getCodeNodeRef() {
return mth.getMethodNode();
}
@Override
public JClass getJParent() {
return jParent;
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeInfo;
import jadx.api.JavaNode;
import jadx.api.metadata.ICodeNodeRef;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
@@ -35,6 +36,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements Comparable
return null;
}
public ICodeNodeRef getCodeNodeRef() {
return null;
}
@Nullable
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
return null;
@@ -11,6 +11,7 @@ import jadx.api.data.ICodeRename;
import jadx.api.data.impl.JadxCodeRef;
import jadx.api.data.impl.JadxCodeRename;
import jadx.api.data.impl.JadxNodeRef;
import jadx.api.metadata.ICodeNodeRef;
import jadx.core.deobf.NameMapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.UiUtils;
@@ -40,6 +41,11 @@ public class JVariable extends JNode implements JRenameNode {
return jMth.getRootClass();
}
@Override
public ICodeNodeRef getCodeNodeRef() {
return var.getVarNode();
}
@Override
public JClass getJParent() {
return jMth.getJParent();
@@ -120,6 +120,7 @@ public final class CodeArea extends AbstractCodeArea {
popup.addSeparator();
popup.add(new FridaAction(this));
popup.add(new XposedAction(this));
getMainWindow().getWrapper().getGuiPluginsContext().appendPopupMenus(this, popup);
// move caret on mouse right button click
popup.getMenu().addPopupMenuListener(new DefaultPopupMenuListener() {
@@ -11,6 +11,7 @@ dependencies {
// manual imports (IDE can't import dependencies by scripts annotations)
implementation("com.github.javafaker:javafaker:1.0.2")
implementation("org.apache.commons:commons-text:1.10.0")
}
sourceSets {
@@ -5,7 +5,7 @@
import jadx.api.plugins.input.insns.Opcode
import jadx.core.dex.nodes.MethodNode
val renamesMap = mapOf(
val renamesMap = mapOf(
"specificString" to "newMethodName",
"AA6" to "aa6Method"
)
@@ -14,7 +14,7 @@ val jadx = getJadxInstance()
var n = 0
jadx.rename.all { _, node ->
var newName : String? = null
var newName: String? = null
if (node is MethodNode) {
// use quick instructions scanner
node.codeReader?.visitInstructions { insn ->
@@ -0,0 +1,120 @@
@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.collectArgsWithoutLoading().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'"
}
@@ -23,7 +23,7 @@ data class ScriptCompletionResult(
)
data class ScriptAnalyzeResult(
val errors: List<ScriptDiagnostic>,
val issues: List<ScriptDiagnostic>,
val renderType: String?,
val reports: List<ScriptDiagnostic>
)
@@ -44,7 +44,7 @@ class ScriptCompiler(private val scriptName: String) {
val result = analyze(code.toScriptSource(scriptName), cursor)
val analyzerResult = result.valueOrNull()
return ScriptAnalyzeResult(
errors = analyzerResult?.get(ReplAnalyzerResult.analysisDiagnostics)?.toList() ?: emptyList(),
issues = analyzerResult?.get(ReplAnalyzerResult.analysisDiagnostics)?.toList() ?: emptyList(),
renderType = analyzerResult?.get(ReplAnalyzerResult.renderedResultType),
reports = result.reports
)
@@ -1,5 +1,6 @@
package jadx.plugins.script.runtime.data
import jadx.api.metadata.ICodeNodeRef
import jadx.api.plugins.gui.JadxGuiContext
import jadx.plugins.script.runtime.JadxScriptInstance
@@ -15,10 +16,30 @@ class Gui(
}
fun ui(block: () -> Unit) {
guiContext?.uiRun(block)
context().uiRun(block)
}
fun addMenuAction(name: String, action: () -> Unit) {
guiContext?.addMenuAction(name, action)
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)
}
private fun context(): JadxGuiContext =
guiContext ?: throw IllegalStateException("GUI plugins context not available!")
}