From acbe94df27271d47b95b47ac94d23ae704b47875 Mon Sep 17 00:00:00 2001 From: Skylot Date: Fri, 4 Nov 2022 21:49:18 +0000 Subject: [PATCH] feat(script): add code area popup menu action --- .../jadx/api/plugins/gui/JadxGuiContext.java | 36 ++++++ .../src/main/java/jadx/gui/JadxWrapper.java | 10 +- .../gui/plugins/context/CodePopupAction.java | 65 ++++++++++ .../plugins/context/GuiPluginsContext.java | 52 +++++++- .../jadx/gui/plugins/script/KtLintUtils.kt | 11 ++ .../gui/plugins/script/ScriptCodeArea.java | 2 + .../plugins/script/ScriptContentPanel.java | 20 +-- .../plugins/script/ScriptErrorService.java | 27 +++- .../main/java/jadx/gui/treemodel/JClass.java | 6 + .../main/java/jadx/gui/treemodel/JField.java | 6 + .../main/java/jadx/gui/treemodel/JMethod.java | 6 + .../main/java/jadx/gui/treemodel/JNode.java | 5 + .../java/jadx/gui/treemodel/JVariable.java | 6 + .../java/jadx/gui/ui/codearea/CodeArea.java | 1 + .../jadx-script/examples/build.gradle.kts | 1 + .../examples/scripts/deobf_by_code.jadx.kts | 4 +- .../scripts/gui_custom_frida.jadx.kts | 120 ++++++++++++++++++ .../src/main/kotlin/ScriptCompiler.kt | 4 +- .../jadx/plugins/script/runtime/data/Gui.kt | 25 +++- 19 files changed, 385 insertions(+), 22 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java create mode 100644 jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts diff --git a/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java b/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java index 7c810d231..46cd7ed27 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java +++ b/jadx-core/src/main/java/jadx/api/plugins/gui/JadxGuiContext.java @@ -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 enabled, + @Nullable String keyBinding, + Consumer 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); } diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index ab14f9cee..0950132cc 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -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 */ diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java new file mode 100644 index 000000000..0551e62db --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/CodePopupAction.java @@ -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 enabledCheck; + private final String keyBinding; + private final Consumer action; + + public CodePopupAction(String name, Function enabled, String keyBinding, Consumer 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); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginsContext.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginsContext.java index 6c8fdc27a..e897bca4d 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginsContext.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiPluginsContext.java @@ -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 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 enabled, + @Nullable String keyBinding, Consumer 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); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/KtLintUtils.kt b/jadx-gui/src/main/java/jadx/gui/plugins/script/KtLintUtils.kt index 4a91edce2..e717552dd 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/KtLintUtils.kt +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/KtLintUtils.kt @@ -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) { diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java index 1c229a613..f05c72db2 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptCodeArea.java @@ -63,7 +63,9 @@ public class ScriptCodeArea extends AbstractCodeArea { } public void updateCode(String newCode) { + int caretPos = getCaretPosition(); setText(newCode); + setCaretPosition(caretPos); scriptNode.setChanged(true); } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java index f85984fd9..c2316aa73 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptContentPanel.java @@ -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 errors = result.getErrors(); - for (ScriptDiagnostic error : errors) { - LOG.warn("Parse error: {}", error); + List 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 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()); } diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java index 511b0fffd..7090746ea 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/script/ScriptErrorService.java @@ -62,20 +62,21 @@ public class ScriptErrorService extends AbstractParser { } } - public void addErrors(List errors) { - for (ScriptDiagnostic error : errors) { + public void addCompilerIssues(List 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 errors) { for (LintError error : errors) { try { diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 854bbf107..2cf17e1aa 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -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; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java index 9c170abba..648166391 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java @@ -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; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java index 66d056fcb..2afe63793 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java @@ -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; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java index 46144bc65..52a3f70f6 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -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; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java index dd8a1fd89..53ad08b2f 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JVariable.java @@ -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(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index 264d48a24..496f8d96c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -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() { diff --git a/jadx-plugins/jadx-script/examples/build.gradle.kts b/jadx-plugins/jadx-script/examples/build.gradle.kts index 1df9cf83c..1dee84578 100644 --- a/jadx-plugins/jadx-script/examples/build.gradle.kts +++ b/jadx-plugins/jadx-script/examples/build.gradle.kts @@ -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 { diff --git a/jadx-plugins/jadx-script/examples/scripts/deobf_by_code.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/deobf_by_code.jadx.kts index a590b2766..d5787e3a2 100644 --- a/jadx-plugins/jadx-script/examples/scripts/deobf_by_code.jadx.kts +++ b/jadx-plugins/jadx-script/examples/scripts/deobf_by_code.jadx.kts @@ -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 -> diff --git a/jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts b/jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts new file mode 100644 index 000000000..6879dfeca --- /dev/null +++ b/jadx-plugins/jadx-script/examples/scripts/gui_custom_frida.jadx.kts @@ -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'" +} diff --git a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptCompiler.kt b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptCompiler.kt index 1936a7acf..59aefa802 100644 --- a/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptCompiler.kt +++ b/jadx-plugins/jadx-script/jadx-script-ide/src/main/kotlin/ScriptCompiler.kt @@ -23,7 +23,7 @@ data class ScriptCompletionResult( ) data class ScriptAnalyzeResult( - val errors: List, + val issues: List, val renderType: String?, val reports: List ) @@ -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 ) diff --git a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Gui.kt b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Gui.kt index 9a8c49786..c0517f331 100644 --- a/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Gui.kt +++ b/jadx-plugins/jadx-script/jadx-script-runtime/src/main/kotlin/jadx/plugins/script/runtime/data/Gui.kt @@ -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!") }