feat: make jadx-script-kotlin plugin external

This commit is contained in:
Skylot
2026-02-10 20:39:29 +00:00
parent 9710ebe09a
commit c7a0f7a092
101 changed files with 1717 additions and 1489 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
implementation("org.openrewrite:plugin:6.19.1")
}
@@ -5,6 +5,11 @@ plugins {
id("org.jetbrains.kotlin.jvm")
}
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect")) // don't work from plugin classloader
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
-1
View File
@@ -19,7 +19,6 @@ dependencies {
runtimeOnly(project(":jadx-plugins:jadx-rename-mappings"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-metadata"))
runtimeOnly(project(":jadx-plugins:jadx-kotlin-source-debug-extension"))
runtimeOnly(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
runtimeOnly(project(":jadx-plugins:jadx-xapk-input"))
runtimeOnly(project(":jadx-plugins:jadx-aab-input"))
runtimeOnly(project(":jadx-plugins:jadx-apkm-input"))
@@ -138,11 +138,16 @@ public final class JadxDecompiler implements Closeable {
loadFinished();
}
/**
* Reload passes and plugins without processing classes and inputs
*/
public void reloadPasses() {
LOG.info("reloading (passes only) ...");
customPasses.clear();
root.resetPasses();
events.reset();
unloadPlugins();
loadPlugins();
root.mergePasses(customPasses);
root.restartVisitors();
@@ -148,7 +148,7 @@ public class JadxPluginManager {
}
context.init();
} catch (Exception e) {
LOG.warn("Failed to init plugin: {}", context.getPluginId(), e);
LOG.error("Failed to init plugin: {}", context.getPluginId(), e);
}
}
for (PluginContext context : pluginContexts) {
@@ -39,6 +39,7 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
private final JadxPluginsData pluginsData;
private final JadxPlugin plugin;
private final JadxPluginInfo pluginInfo;
private final ClassLoader pluginClassLoader;
private AppContext appContext;
@@ -53,16 +54,30 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
this.pluginsData = pluginsData;
this.plugin = plugin;
this.pluginInfo = plugin.getPluginInfo();
this.pluginClassLoader = plugin.getClass().getClassLoader();
}
public void init() {
plugin.init(this);
initialized = true;
classLoaderWrap(() -> {
plugin.init(this);
initialized = true;
});
}
public void unload() {
if (initialized) {
plugin.unload();
classLoaderWrap(plugin::unload);
}
}
public void classLoaderWrap(Runnable task) {
Thread thread = Thread.currentThread();
ClassLoader prevClassLoader = thread.getContextClassLoader();
thread.setContextClassLoader(pluginClassLoader);
try {
task.run();
} finally {
thread.setContextClassLoader(prevClassLoader);
}
}
+2 -11
View File
@@ -16,21 +16,12 @@ dependencies {
// import mappings
implementation(project(":jadx-plugins:jadx-rename-mappings"))
// jadx-script autocomplete support
implementation(project(":jadx-plugins:jadx-script:jadx-script-ide"))
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
implementation(kotlin("scripting-common"))
implementation("com.fifesoft:autocomplete:3.3.2")
// 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")
implementation("org.jcommander:jcommander:2.0")
implementation("ch.qos.logback:logback-classic:1.5.21")
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
implementation("com.fifesoft:rsyntaxtextarea:3.6.0")
implementation("com.fifesoft:rsyntaxtextarea:3.6.1")
implementation("com.fifesoft:autocomplete:3.3.2")
implementation("org.drjekyll:fontchooser:3.1.0")
implementation("hu.kazocsaba:image-viewer:1.2.3")
implementation("com.twelvemonkeys.imageio:imageio-webp:3.12.0") // WebP support for image viewer
@@ -21,6 +21,9 @@ import jadx.api.JavaPackage;
import jadx.api.ResourceFile;
import jadx.api.impl.InMemoryCodeCache;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.plugins.pass.JadxPassInfo;
import jadx.api.plugins.pass.impl.SimpleJadxPassInfo;
import jadx.api.plugins.pass.types.JadxPreparePass;
import jadx.api.usage.impl.EmptyUsageInfoCache;
import jadx.api.usage.impl.InMemoryUsageInfoCache;
import jadx.cli.JadxAppCommon;
@@ -30,6 +33,7 @@ import jadx.core.dex.nodes.ProcessState;
import jadx.core.dex.nodes.RootNode;
import jadx.core.plugins.AppContext;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.code.CodeStringCache;
import jadx.gui.cache.code.disk.BufferCodeCache;
import jadx.gui.cache.code.disk.DiskCodeCache;
@@ -73,10 +77,9 @@ public class JadxWrapper {
decompiler = new JadxDecompiler(jadxArgs);
guiPluginsContext = initGuiPluginsContext(decompiler, mainWindow);
initUsageCache(jadxArgs);
registerCodeCache(decompiler);
decompiler.setEventsImpl(mainWindow.events());
decompiler.load();
initCodeCache();
}
} catch (Exception e) {
LOG.error("Jadx decompiler wrapper init error", e);
@@ -114,22 +117,39 @@ public class JadxWrapper {
}
}
private void initCodeCache() {
switch (getSettings().getCodeCacheMode()) {
case MEMORY:
getArgs().setCodeCache(new InMemoryCodeCache());
break;
case DISK_WITH_CACHE:
getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache()));
break;
case DISK:
getArgs().setCodeCache(buildBufferedDiskCache());
break;
/**
* Disk cache require loaded classes to operate, but cache should be set before 'after load' event
* to allow plugins decompile classes with cache enabled.
* To resolve this, register last 'prepare' pass for cache initialization.
*/
private void registerCodeCache(JadxDecompiler jadxDecompiler) {
CodeCacheMode codeCacheMode = getSettings().getCodeCacheMode();
if (codeCacheMode == CodeCacheMode.MEMORY) {
jadxDecompiler.getArgs().setCodeCache(new InMemoryCodeCache());
return;
}
jadxDecompiler.addCustomPass(new JadxPreparePass() {
@Override
public JadxPassInfo getInfo() {
return new SimpleJadxPassInfo("CacheInit");
}
@Override
public void init(RootNode root) {
switch (getSettings().getCodeCacheMode()) {
case DISK_WITH_CACHE:
root.getArgs().setCodeCache(new CodeStringCache(buildBufferedDiskCache(root)));
break;
case DISK:
root.getArgs().setCodeCache(buildBufferedDiskCache(root));
break;
}
}
});
}
private BufferCodeCache buildBufferedDiskCache() {
DiskCodeCache diskCache = new DiskCodeCache(getDecompiler().getRoot(), getProject().getCacheDir());
private BufferCodeCache buildBufferedDiskCache(RootNode root) {
DiskCodeCache diskCache = new DiskCodeCache(root, getProject().getCacheDir());
return new BufferCodeCache(diskCache);
}
@@ -6,8 +6,6 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.utils.UiUtils;
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
class LogAppender implements ILogListener {
private final LogOptions options;
private final RSyntaxTextArea textArea;
@@ -39,7 +37,7 @@ class LogAppender implements ILogListener {
return true;
case ALL_SCRIPTS:
return logEvent.getLoggerName().startsWith(JADX_SCRIPT_LOG_PREFIX);
return logEvent.getLoggerName().startsWith("JadxScript:");
case CURRENT_SCRIPT:
return logEvent.getLoggerName().equals(options.getFilter());
@@ -6,8 +6,6 @@ import ch.qos.logback.classic.Level;
import jadx.core.utils.Utils;
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
public class LogOptions {
/**
@@ -30,7 +28,7 @@ public class LogOptions {
}
public static LogOptions forScript(String scriptName) {
String filter = JADX_SCRIPT_LOG_PREFIX + scriptName;
String filter = "JadxScript:" + scriptName;
return store(new LogOptions(LogMode.CURRENT_SCRIPT, current.getLogLevel(), filter));
}
@@ -19,7 +19,6 @@ import org.jetbrains.annotations.Nullable;
import ch.qos.logback.classic.Level;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.AbstractCodeArea;
@@ -144,7 +143,7 @@ public class LogPanel extends JPanel {
TabBlueprint selectedTab = mainWindow.getTabsController().getSelectedTab();
if (selectedTab != null) {
JNode node = selectedTab.getNode();
if (node instanceof JInputScript) {
if (node.getClass().getSimpleName().equals("JInputScript")) { // TODO: register custom log filters
return node.getName();
}
}
@@ -10,6 +10,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.plugins.PluginContext;
import jadx.gui.settings.data.ITabStatePersist;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.JNodePopupBuilder;
@@ -23,6 +24,8 @@ public class CommonGuiPluginsContext {
private final List<CodePopupAction> codePopupActionList = new ArrayList<>();
private final List<TreePopupMenuEntry> treePopupMenuEntries = new ArrayList<>();
private final List<ITreeInputCategory> treeInputCategories = new ArrayList<>();
private final List<ITabStatePersist> tabStatePersistAdapters = new ArrayList<>();
public CommonGuiPluginsContext(MainWindow mainWindow) {
this.mainWindow = mainWindow;
@@ -38,9 +41,19 @@ public class CommonGuiPluginsContext {
return pluginsMap.get(pluginContext);
}
public @Nullable GuiPluginContext getGuiPluginContextById(String pluginId) {
for (GuiPluginContext guiPluginContext : pluginsMap.values()) {
if (guiPluginContext.getPluginContext().getPluginId().equals(pluginId)) {
return guiPluginContext;
}
}
return null;
}
public void reset() {
codePopupActionList.clear();
treePopupMenuEntries.clear();
treeInputCategories.clear();
mainWindow.resetPluginsMenu();
}
@@ -56,6 +69,14 @@ public class CommonGuiPluginsContext {
return treePopupMenuEntries;
}
public List<ITreeInputCategory> getTreeInputCategories() {
return treeInputCategories;
}
public List<ITabStatePersist> getTabStatePersistAdapters() {
return tabStatePersistAdapters;
}
public void addMenuAction(String name, Runnable action) {
ActionHandler item = new ActionHandler(ev -> {
try {
@@ -26,6 +26,7 @@ import jadx.api.plugins.gui.JadxGuiContext;
import jadx.api.plugins.gui.JadxGuiSettings;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.data.ITabStatePersist;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.AbstractCodeContentPanel;
@@ -82,6 +83,14 @@ public class GuiPluginContext implements JadxGuiContext {
commonContext.getTreePopupMenuEntries().add(new TreePopupMenuEntry(name, addPredicate, action));
}
public void registerTreeInputCategory(ITreeInputCategory inputCategory) {
commonContext.getTreeInputCategories().add(inputCategory);
}
public void registerTabStatePersistAdapter(ITabStatePersist tabStatePersist) {
commonContext.getTabStatePersistAdapters().add(tabStatePersist);
}
@Override
public boolean registerGlobalKeyBinding(String id, String keyBinding, Runnable action) {
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyBinding);
@@ -0,0 +1,25 @@
package jadx.gui.plugins.context;
import java.nio.file.Path;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import jadx.gui.treemodel.JNode;
/**
* Custom category for 'Inputs' tree section
*/
@ApiStatus.Experimental
public interface ITreeInputCategory {
/**
* Check if file should be moved into this category
*/
boolean filesFilter(Path file);
/**
* Build node for filtered files
*/
JNode buildInputNode(List<Path> files);
}
@@ -1,90 +0,0 @@
package jadx.gui.plugins.script;
import org.fife.ui.autocomplete.AutoCompletion;
import org.jetbrains.annotations.NotNull;
import jadx.api.ICodeInfo;
import jadx.gui.jobs.IBackgroundTask;
import jadx.gui.jobs.LoadTask;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.ui.action.JadxAutoCompletion;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.shortcut.ShortcutsController;
public class ScriptCodeArea extends AbstractCodeArea {
private final JInputScript scriptNode;
private final AutoCompletion autoCompletion;
private final ShortcutsController shortcutsController;
public ScriptCodeArea(ContentPanel contentPanel, JInputScript node) {
super(contentPanel, node);
scriptNode = node;
setSyntaxEditingStyle(node.getSyntaxName());
setCodeFoldingEnabled(true);
setCloseCurlyBraces(true);
shortcutsController = contentPanel.getMainWindow().getShortcutsController();
JadxSettings settings = contentPanel.getMainWindow().getSettings();
autoCompletion = addAutoComplete(settings);
}
private AutoCompletion addAutoComplete(JadxSettings settings) {
ScriptCompleteProvider provider = new ScriptCompleteProvider(this);
provider.setAutoActivationRules(false, ".");
JadxAutoCompletion ac = new JadxAutoCompletion(provider);
ac.setListCellRenderer(new ScriptCompletionRenderer(settings));
ac.setAutoActivationEnabled(true);
ac.setAutoCompleteSingleChoices(true);
ac.install(this);
shortcutsController.bindImmediate(ac);
return ac;
}
@Override
public @NotNull ICodeInfo getCodeInfo() {
return node.getCodeInfo();
}
@Override
public IBackgroundTask getLoadTask() {
return new LoadTask<>(
() -> node.getCodeInfo().getCodeStr(),
code -> {
setText(code);
setCaretPosition(0);
setLoaded();
});
}
@Override
public void refresh() {
setText(node.getCodeInfo().getCodeStr());
}
public void updateCode(String newCode) {
int caretPos = getCaretPosition();
setText(newCode);
setCaretPosition(caretPos);
scriptNode.setChanged(true);
}
public void save() {
scriptNode.save(getText());
scriptNode.setChanged(false);
}
public JInputScript getScriptNode() {
return scriptNode;
}
@Override
public void dispose() {
shortcutsController.unbindActionsForComponent(this);
autoCompletion.uninstall();
super.dispose();
}
}
@@ -1,157 +0,0 @@
package jadx.gui.plugins.script;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.swing.Icon;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.autocomplete.CompletionProviderBase;
import org.fife.ui.autocomplete.ParameterizedCompletion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import kotlin.script.experimental.api.ScriptDiagnostic;
import kotlin.script.experimental.api.SourceCode;
import kotlin.script.experimental.api.SourceCodeCompletionVariant;
import jadx.core.utils.ListUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.utils.Icons;
import jadx.plugins.script.ide.ScriptCompletionResult;
import jadx.plugins.script.ide.ScriptServices;
import static jadx.plugins.script.ide.ScriptServicesKt.AUTO_COMPLETE_INSERT_STR;
public class ScriptCompleteProvider extends CompletionProviderBase {
private static final Logger LOG = LoggerFactory.getLogger(ScriptCompleteProvider.class);
private static final Map<String, Icon> ICONS_MAP = buildIconsMap();
private static Map<String, Icon> buildIconsMap() {
Map<String, Icon> map = new HashMap<>();
map.put("class", Icons.CLASS);
map.put("method", Icons.METHOD);
map.put("field", Icons.FIELD);
map.put("property", Icons.PROPERTY);
map.put("parameter", Icons.PARAMETER);
map.put("package", Icons.PACKAGE);
return map;
}
private final AbstractCodeArea codeArea;
private ScriptServices scriptServices;
public ScriptCompleteProvider(AbstractCodeArea codeArea) {
this.codeArea = codeArea;
}
private List<Completion> getCompletions() {
try {
String code = codeArea.getText();
int caretPos = codeArea.getCaretPosition();
// TODO: resolve error after reusing ScriptCompiler
scriptServices = new ScriptServices();
String scriptName = codeArea.getNode().getName();
ScriptCompletionResult result = scriptServices.complete(scriptName, code, caretPos);
int replacePos = getReplacePos(caretPos, result);
if (!result.getReports().isEmpty()) {
LOG.debug("Script completion reports: {}", result.getReports());
}
return convertCompletions(result.getCompletions(), code, replacePos);
} catch (Exception e) {
LOG.error("Code completion failed", e);
return Collections.emptyList();
}
}
private List<Completion> convertCompletions(List<SourceCodeCompletionVariant> completions, String code, int replacePos) {
if (completions.isEmpty()) {
return Collections.emptyList();
}
if (LOG.isDebugEnabled()) {
String cmplStr = completions.stream().map(SourceCodeCompletionVariant::toString).collect(Collectors.joining("\n"));
LOG.debug("Completions:\n{}", cmplStr);
}
int count = completions.size();
List<Completion> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
SourceCodeCompletionVariant c = completions.get(i);
if (Objects.equals(c.getIcon(), "keyword")) {
// too many, not very useful
continue;
}
ScriptCompletionData cmpl = new ScriptCompletionData(this, count - i);
cmpl.setData(c.getText(), code, replacePos);
if (Objects.equals(c.getIcon(), "method") && !Objects.equals(c.getText(), c.getDisplayText())) {
// add method args details for methods
cmpl.setSummary(c.getDisplayText() + " " + c.getTail());
} else {
cmpl.setSummary(c.getTail());
}
cmpl.setToolTip(c.getDisplayText());
Icon icon = ICONS_MAP.get(c.getIcon());
cmpl.setIcon(icon != null ? icon : Icons.FILE);
list.add(cmpl);
}
return list;
}
private int getReplacePos(int caretPos, ScriptCompletionResult result) throws BadLocationException {
int lineRaw = codeArea.getLineOfOffset(caretPos);
int lineStart = codeArea.getLineStartOffset(lineRaw);
int line = lineRaw + 1;
int col = caretPos - lineStart + 1;
List<ScriptDiagnostic> reports = result.getReports();
ScriptDiagnostic cmplReport = ListUtils.filterOnlyOne(reports, r -> {
if (r.getSeverity() == ScriptDiagnostic.Severity.ERROR && r.getLocation() != null) {
SourceCode.Position start = r.getLocation().getStart();
return start.getLine() == line && r.getMessage().endsWith(AUTO_COMPLETE_INSERT_STR);
}
return false;
});
if (cmplReport == null) {
LOG.warn("Failed to find completion report in: {}", reports);
return caretPos;
}
reports.remove(cmplReport);
int reportCol = Objects.requireNonNull(cmplReport.getLocation()).getStart().getCol();
return caretPos - (col - reportCol);
}
@Override
public String getAlreadyEnteredText(JTextComponent comp) {
try {
int pos = codeArea.getCaretPosition();
return codeArea.getText(0, pos);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to get text before caret", e);
}
}
@Override
public List<Completion> getCompletionsAt(JTextComponent comp, Point p) {
return getCompletions();
}
@Override
protected List<Completion> getCompletionsImpl(JTextComponent comp) {
return getCompletions();
}
@Override
public List<ParameterizedCompletion> getParameterizedCompletions(JTextComponent tc) {
return null;
}
}
@@ -1,93 +0,0 @@
package jadx.gui.plugins.script;
import javax.swing.Icon;
import javax.swing.text.JTextComponent;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.autocomplete.CompletionProvider;
public class ScriptCompletionData implements Completion {
private final CompletionProvider provider;
private final int relevance;
private String input;
private String code;
private int replacePos;
private Icon icon;
private String summary;
private String toolTip;
public ScriptCompletionData(CompletionProvider provider, int relevance) {
this.provider = provider;
this.relevance = relevance;
}
public void setData(String input, String code, int replacePos) {
this.input = input;
this.code = code;
this.replacePos = replacePos;
}
@Override
public String getInputText() {
return input;
}
@Override
public CompletionProvider getProvider() {
return provider;
}
@Override
public String getAlreadyEntered(JTextComponent comp) {
return provider.getAlreadyEnteredText(comp);
}
@Override
public int getRelevance() {
return relevance;
}
@Override
public String getReplacementText() {
return code.substring(0, replacePos) + input;
}
@Override
public Icon getIcon() {
return icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
@Override
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
@Override
public String getToolTipText() {
return toolTip;
}
public void setToolTip(String toolTip) {
this.toolTip = toolTip;
}
@Override
public int compareTo(Completion other) {
return Integer.compare(relevance, other.getRelevance());
}
@Override
public String toString() {
return input;
}
}
@@ -1,26 +0,0 @@
package jadx.gui.plugins.script;
import javax.swing.JList;
import org.fife.ui.autocomplete.Completion;
import org.fife.ui.autocomplete.CompletionCellRenderer;
import jadx.gui.settings.JadxSettings;
import static jadx.gui.utils.UiUtils.escapeHtml;
import static jadx.gui.utils.UiUtils.fadeHtml;
import static jadx.gui.utils.UiUtils.wrapHtml;
public class ScriptCompletionRenderer extends CompletionCellRenderer {
public ScriptCompletionRenderer(JadxSettings settings) {
setDisplayFont(settings.getCodeFont());
}
@Override
protected void prepareForOtherCompletion(JList list, Completion c, int index, boolean selected, boolean hasFocus) {
ScriptCompletionData cmpl = (ScriptCompletionData) c;
setText(wrapHtml(escapeHtml(cmpl.getInputText()) + " "
+ fadeHtml(escapeHtml(cmpl.getSummary()))));
}
}
@@ -1,255 +0,0 @@
package jadx.gui.plugins.script;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.util.Collections;
import java.util.List;
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 org.fife.ui.rsyntaxtextarea.ErrorStrip;
import org.fife.ui.rtextarea.RTextScrollPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import kotlin.script.experimental.api.ScriptDiagnostic;
import kotlin.script.experimental.api.ScriptDiagnostic.Severity;
import jadx.gui.logs.LogOptions;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.ui.MainWindow;
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.ide.ScriptAnalyzeResult;
import jadx.plugins.script.ide.ScriptServices;
import static jadx.plugins.script.runtime.ScriptRuntime.JADX_SCRIPT_LOG_PREFIX;
public class ScriptContentPanel extends AbstractCodeContentPanel {
private static final long serialVersionUID = 6575696321112417513L;
private final ScriptCodeArea scriptArea;
private final SearchBar searchBar;
private final RTextScrollPane codeScrollPane;
private final JPanel actionPanel;
private final JLabel resultLabel;
private final ScriptErrorService errorService;
private final Logger scriptLog;
public ScriptContentPanel(TabbedPane panel, JInputScript scriptNode) {
super(panel, scriptNode);
scriptArea = new ScriptCodeArea(this, scriptNode);
resultLabel = new NodeLabel("");
errorService = new ScriptErrorService(scriptArea);
actionPanel = buildScriptActionsPanel();
searchBar = new SearchBar(scriptArea);
codeScrollPane = new RTextScrollPane(scriptArea);
scriptLog = LoggerFactory.getLogger(JADX_SCRIPT_LOG_PREFIX + scriptNode.getName());
initUI();
applySettings();
scriptArea.load();
}
private void initUI() {
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
topPanel.add(actionPanel, BorderLayout.NORTH);
topPanel.add(searchBar, BorderLayout.SOUTH);
JPanel codePanel = new JPanel(new BorderLayout());
codePanel.setBorder(new EmptyBorder(0, 0, 0, 0));
codePanel.add(codeScrollPane);
codePanel.add(new ErrorStrip(scriptArea), BorderLayout.LINE_END);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
add(topPanel, BorderLayout.NORTH);
add(codeScrollPane, BorderLayout.CENTER);
KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, UiUtils.ctrlButton());
UiUtils.addKeyBinding(scriptArea, key, "SearchAction", searchBar::toggle);
}
private JPanel buildScriptActionsPanel() {
JadxGuiAction runAction = new JadxGuiAction(ActionModel.SCRIPT_RUN, this::runScript);
JadxGuiAction saveAction = new JadxGuiAction(ActionModel.SCRIPT_SAVE, scriptArea::save);
runAction.setShortcutComponent(scriptArea);
saveAction.setShortcutComponent(scriptArea);
tabbedPane.getMainWindow().getShortcutsController().bindImmediate(runAction);
tabbedPane.getMainWindow().getShortcutsController().bindImmediate(saveAction);
JButton save = saveAction.makeButton();
scriptArea.getScriptNode().addChangeListener(save::setEnabled);
JButton check = new JButton(NLS.str("script.check"), Icons.CHECK);
check.addActionListener(ev -> checkScript());
JButton format = new JButton(NLS.str("script.format"), Icons.FORMAT);
format.addActionListener(ev -> reformatCode());
JButton scriptLog = new JButton(NLS.str("script.log"), Icons.FORMAT);
scriptLog.addActionListener(ev -> showScriptLog());
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
panel.setBorder(new EmptyBorder(0, 0, 0, 0));
panel.add(runAction.makeButton());
panel.add(Box.createRigidArea(new Dimension(10, 0)));
panel.add(save);
panel.add(Box.createRigidArea(new Dimension(10, 0)));
panel.add(check);
panel.add(Box.createRigidArea(new Dimension(10, 0)));
panel.add(format);
panel.add(Box.createRigidArea(new Dimension(30, 0)));
panel.add(resultLabel);
panel.add(Box.createHorizontalGlue());
panel.add(scriptLog);
return panel;
}
private void runScript() {
scriptArea.save();
if (!checkScript()) {
return;
}
resetResultLabel();
TabbedPane tabbedPane = getTabbedPane();
MainWindow mainWindow = tabbedPane.getMainWindow();
mainWindow.getBackgroundExecutor().execute(NLS.str("script.run"), () -> {
try {
mainWindow.getWrapper().reloadPasses();
} catch (Exception e) {
scriptLog.error("Passes reload failed", e);
}
}, taskStatus -> {
mainWindow.passesReloaded();
});
}
private boolean checkScript() {
try {
resetResultLabel();
String code = scriptArea.getText();
String fileName = scriptArea.getNode().getName();
ScriptServices scriptServices = new ScriptServices();
ScriptAnalyzeResult result = scriptServices.analyze(fileName, code);
boolean success = result.getSuccess();
List<ScriptDiagnostic> issues = result.getIssues();
for (ScriptDiagnostic issue : issues) {
Severity severity = issue.getSeverity();
if (severity == Severity.ERROR || severity == Severity.FATAL) {
scriptLog.error("{}", issue.render(false, true, true, true));
success = false;
} else if (severity == Severity.WARNING) {
scriptLog.warn("Compile issue: {}", issue);
}
}
List<JadxLintError> lintErrs = Collections.emptyList();
if (success) {
lintErrs = getLintIssues(code);
}
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 (Throwable e) {
scriptLog.error("Failed to check code", e);
return true;
}
}
private List<JadxLintError> getLintIssues(String code) {
try {
List<JadxLintError> lintErrs = KtLintUtils.INSTANCE.lint(code);
for (JadxLintError error : lintErrs) {
scriptLog.warn("Lint issue: {} ({}:{})(ruleId={})",
error.getDetail(), error.getLine(), error.getCol(), error.getRuleId());
}
return lintErrs;
} catch (Throwable e) { // can throw initialization error
scriptLog.warn("KtLint failed", e);
return Collections.emptyList();
}
}
private void reformatCode() {
resetResultLabel();
try {
String code = scriptArea.getText();
String formattedCode = KtLintUtils.INSTANCE.format(code);
if (!code.equals(formattedCode)) {
scriptArea.updateCode(formattedCode);
resultLabel.setText("Code updated");
errorService.clearErrors();
}
} catch (Throwable e) { // can throw initialization error
scriptLog.error("Failed to reformat code", e);
}
}
private void resetResultLabel() {
resultLabel.setText("");
}
private void applySettings() {
JadxSettings settings = getSettings();
codeScrollPane.setLineNumbersEnabled(settings.getLineNumbersMode() != LineNumbersMode.DISABLE);
codeScrollPane.getGutter().setLineNumberFont(settings.getCodeFont());
scriptArea.loadSettings();
}
private void showScriptLog() {
getMainWindow().showLogViewer(LogOptions.forScript(getNode().getName()));
}
@Override
public AbstractCodeArea getCodeArea() {
return scriptArea;
}
@Override
public Component getChildrenComponent() {
return getCodeArea();
}
@Override
public void loadSettings() {
applySettings();
updateUI();
}
public void dispose() {
scriptArea.dispose();
}
}
@@ -1,124 +0,0 @@
package jadx.gui.plugins.script;
import java.util.List;
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;
import kotlin.script.experimental.api.SourceCode;
public class ScriptErrorService extends AbstractParser {
private static final Logger LOG = LoggerFactory.getLogger(ScriptErrorService.class);
private final DefaultParseResult result;
private final ScriptCodeArea scriptArea;
public ScriptErrorService(ScriptCodeArea scriptArea) {
this.scriptArea = scriptArea;
this.result = new DefaultParseResult(this);
}
@Override
public ParseResult parse(RSyntaxDocument doc, String style) {
return result;
}
public void clearErrors() {
result.clearNotices();
scriptArea.removeParser(this);
}
public void apply() {
scriptArea.removeParser(this);
scriptArea.addParser(this);
scriptArea.addNotify();
scriptArea.requestFocus();
jumpCaretToFirstError();
}
private void jumpCaretToFirstError() {
List<ParserNotice> parserNotices = result.getNotices();
if (parserNotices.isEmpty()) {
return;
}
ParserNotice notice = parserNotices.get(0);
int offset = notice.getOffset();
if (offset == -1) {
try {
offset = scriptArea.getLineStartOffset(notice.getLine());
} catch (Exception e) {
LOG.error("Failed to jump to first error", e);
return;
}
}
scriptArea.scrollToPos(offset);
}
public void addCompilerIssues(List<ScriptDiagnostic> issues) {
for (ScriptDiagnostic issue : issues) {
if (issue.getSeverity() == ScriptDiagnostic.Severity.DEBUG) {
continue;
}
DefaultParserNotice notice;
SourceCode.Location loc = issue.getLocation();
if (loc == null) {
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, issue.getMessage(), line, offset - 1, len);
notice.setLevel(convertLevel(issue.getSeverity()));
} catch (Exception e) {
LOG.error("Failed to convert script issue", e);
continue;
}
}
addNotice(notice);
}
}
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<JadxLintError> errors) {
for (JadxLintError error : errors) {
try {
int line = error.getLine();
int offset = scriptArea.getLineStartOffset(line - 1) + error.getCol() - 1;
String word = scriptArea.getWordByPosition(offset);
int len = word != null ? word.length() : -1;
DefaultParserNotice notice = new DefaultParserNotice(this, error.getDetail(), line, offset, len);
notice.setLevel(ParserNotice.Level.WARNING);
addNotice(notice);
} catch (Exception e) {
LOG.error("Failed to convert lint error", e);
}
}
}
private void addNotice(DefaultParserNotice notice) {
LOG.debug("Add notice: {}:{}:{} - {}",
notice.getLine(), notice.getOffset(), notice.getLength(), notice.getMessage());
result.addNotice(notice);
}
}
@@ -53,6 +53,7 @@ public class JadxProject {
private static final int SEARCH_HISTORY_LIMIT = 30;
private final transient MainWindow mainWindow;
private final transient TabStateViewAdapter tabStateViewAdapter = new TabStateViewAdapter();
private transient String name = "New Project";
private transient @Nullable Path projectPath;
@@ -155,7 +156,7 @@ public class JadxProject {
public void saveOpenTabs(List<EditorViewState> tabs) {
List<TabViewState> tabStateList = tabs.stream()
.map(TabStateViewAdapter::build)
.map(tabStateViewAdapter::build)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (data.setOpenTabs(tabStateList)) {
@@ -164,8 +165,9 @@ public class JadxProject {
}
public List<EditorViewState> getOpenTabs(MainWindow mw) {
tabStateViewAdapter.setCustomAdapters(mw.getWrapper().getGuiPluginsContext().getTabStatePersistAdapters());
return data.getOpenTabs().stream()
.map(s -> TabStateViewAdapter.load(mw, s))
.map(s -> tabStateViewAdapter.load(mw, s))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@@ -1,15 +1,19 @@
package jadx.gui.settings;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JavaClass;
import jadx.gui.plugins.mappings.JInputMapping;
import jadx.gui.settings.data.ITabStatePersist;
import jadx.gui.settings.data.TabViewState;
import jadx.gui.settings.data.ViewPoint;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JInputScript;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.treemodel.JSubResource;
@@ -20,8 +24,9 @@ import jadx.gui.utils.UiUtils;
public class TabStateViewAdapter {
private static final Logger LOG = LoggerFactory.getLogger(TabStateViewAdapter.class);
@Nullable
public static TabViewState build(EditorViewState viewState) {
private final Map<String, ITabStatePersist> customAdaptersMap = new HashMap<>();
public @Nullable TabViewState build(EditorViewState viewState) {
TabViewState tvs = new TabViewState();
tvs.setSubPath(viewState.getSubPath());
if (!saveJNode(tvs, viewState.getNode())) {
@@ -40,8 +45,7 @@ public class TabStateViewAdapter {
return tvs;
}
@Nullable
public static EditorViewState load(MainWindow mw, TabViewState tvs) {
public @Nullable EditorViewState load(MainWindow mw, TabViewState tvs) {
try {
JNode node = loadJNode(mw, tvs);
if (node == null) {
@@ -63,8 +67,15 @@ public class TabStateViewAdapter {
}
}
public void setCustomAdapters(List<ITabStatePersist> customAdapters) {
customAdaptersMap.clear();
for (ITabStatePersist customAdapter : customAdapters) {
customAdaptersMap.put(customAdapter.getNodeClass().getName(), customAdapter);
}
}
@Nullable
private static JNode loadJNode(MainWindow mw, TabViewState tvs) {
private JNode loadJNode(MainWindow mw, TabViewState tvs) {
switch (tvs.getType()) {
case "class":
JavaClass javaClass = mw.getWrapper().searchJavaClassByRawName(tvs.getTabPath());
@@ -85,18 +96,21 @@ public class TabStateViewAdapter {
}
return null;
case "script":
return mw.getTreeRoot()
.followStaticPath("JInputs", "JInputScripts")
.searchNode(node -> node instanceof JInputScript && node.getName().equals(tvs.getTabPath()));
case "mapping":
return mw.getTreeRoot().followStaticPath("JInputs").searchNode(node -> node instanceof JInputMapping);
}
ITabStatePersist statePersist = customAdaptersMap.get(tvs.getType());
if (statePersist != null) {
try {
return statePersist.load(tvs.getTabPath());
} catch (Exception e) {
LOG.error("Failed to restore tab for custom node adapter: {}", tvs.getType(), e);
}
}
return null;
}
private static boolean saveJNode(TabViewState tvs, JNode node) {
private boolean saveJNode(TabViewState tvs, JNode node) {
if (node instanceof JClass) {
tvs.setType("class");
tvs.setTabPath(((JClass) node).getCls().getRawName());
@@ -113,15 +127,22 @@ public class TabStateViewAdapter {
tvs.setTabPath(node.getName());
return true;
}
if (node instanceof JInputScript) {
tvs.setType("script");
tvs.setTabPath(node.getName());
return true;
}
if (node instanceof JInputMapping) {
tvs.setType("mapping");
return true;
}
String typeName = node.getClass().getName();
ITabStatePersist statePersist = customAdaptersMap.get(typeName);
if (statePersist != null) {
try {
tvs.setTabPath(statePersist.save(node));
tvs.setType(statePersist.getNodeClass().getName());
return true;
} catch (Exception e) {
LOG.error("Failed to save state for custom node: {}", typeName, e);
}
}
return false;
}
}
@@ -0,0 +1,18 @@
package jadx.gui.settings.data;
import org.jetbrains.annotations.Nullable;
import jadx.gui.treemodel.JNode;
/**
* Adapter interface to allow save/load state of opened tabs
*/
public interface ITabStatePersist {
Class<? extends JNode> getNodeClass();
String save(JNode node);
@Nullable
JNode load(String stateStr);
}
@@ -1,107 +0,0 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.impl.SimpleCodeInfo;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.plugins.script.ScriptContentPanel;
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;
public class JInputScript extends JEditableNode {
private static final Logger LOG = LoggerFactory.getLogger(JInputScript.class);
private static final ImageIcon SCRIPT_ICON = UiUtils.openSvgIcon("nodes/kotlin_script");
private final Path scriptPath;
private final String name;
public JInputScript(Path scriptPath) {
this.scriptPath = scriptPath;
this.name = scriptPath.getFileName().toString().replace(".jadx.kts", "");
}
@Override
public boolean hasContent() {
return true;
}
@Override
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
return new ScriptContentPanel(tabbedPane, this);
}
@Override
public @NotNull ICodeInfo getCodeInfo() {
try {
return new SimpleCodeInfo(FileUtils.readFile(scriptPath));
} catch (Exception e) {
throw new JadxRuntimeException("Failed to read script file: " + scriptPath.toAbsolutePath(), e);
}
}
@Override
public void save(String newContent) {
try {
FileUtils.writeFile(scriptPath, newContent);
LOG.debug("Script saved: {}", scriptPath.toAbsolutePath());
} catch (Exception e) {
throw new JadxRuntimeException("Failed to write script file: " + scriptPath.toAbsolutePath(), e);
}
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
JPopupMenu menu = new JPopupMenu();
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
menu.add(new SimpleMenuItem(NLS.str("popup.remove"), () -> mainWindow.removeInput(scriptPath)));
menu.add(new SimpleMenuItem(NLS.str("popup.rename"), () -> mainWindow.renameInput(scriptPath)));
return menu;
}
@Override
public String getSyntaxName() {
return SyntaxConstants.SYNTAX_STYLE_KOTLIN;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public Icon getIcon() {
return SCRIPT_ICON;
}
@Override
public String getName() {
return name;
}
@Override
public String makeString() {
return name;
}
@Override
public String getTooltip() {
return scriptPath.normalize().toAbsolutePath().toString();
}
}
@@ -1,51 +0,0 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.ui.SimpleMenuItem;
public class JInputScripts extends JNode {
private static final ImageIcon INPUT_SCRIPTS_ICON = UiUtils.openSvgIcon("nodes/scriptsModel");
public JInputScripts(List<Path> scripts) {
for (Path script : scripts) {
add(new JInputScript(script));
}
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
JPopupMenu menu = new JPopupMenu();
menu.add(new SimpleMenuItem(NLS.str("popup.add_scripts"), mainWindow::addFiles));
menu.add(new SimpleMenuItem(NLS.str("popup.new_script"), mainWindow::addNewScript));
return menu;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public Icon getIcon() {
return INPUT_SCRIPTS_ICON;
}
@Override
public String getID() {
return "JInputScripts";
}
@Override
public String makeString() {
return NLS.str("tree.input_scripts");
}
}
@@ -1,38 +1,29 @@
package jadx.gui.treemodel;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import jadx.core.utils.files.FileUtils;
import jadx.gui.JadxWrapper;
import jadx.gui.settings.JadxProject;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.plugins.TreeInputsHelper;
public class JInputs extends JNode {
private static final ImageIcon INPUTS_ICON = UiUtils.openSvgIcon("nodes/projectStructure");
public JInputs(JadxWrapper wrapper) {
JadxProject project = wrapper.getProject();
public JInputs(MainWindow mainWindow) {
JadxProject project = mainWindow.getProject();
List<Path> inputs = project.getFilePaths();
List<Path> files = FileUtils.expandDirs(inputs);
List<Path> scripts = new ArrayList<>();
Iterator<Path> it = files.iterator();
while (it.hasNext()) {
Path file = it.next();
if (file.getFileName().toString().endsWith(".jadx.kts")) {
scripts.add(file);
it.remove();
}
}
add(new JInputFiles(files));
add(new JInputScripts(scripts));
TreeInputsHelper inputsHelper = new TreeInputsHelper(mainWindow);
inputsHelper.processInputs(files);
add(new JInputFiles(inputsHelper.getSimpleFiles()));
inputsHelper.getCustomNodes().forEach(this::add);
}
@Override
@@ -19,6 +19,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.JadxWrapper;
import jadx.gui.settings.JadxProject;
import jadx.gui.treemodel.JResource.JResType;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
@@ -28,18 +29,20 @@ public class JRoot extends JNode {
private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/rootPackageFolder");
private final transient JadxWrapper wrapper;
private final transient MainWindow mainWindow;
private transient boolean flatPackages = false;
private final List<JNode> customNodes = new ArrayList<>();
private final transient List<JNode> customNodes = new ArrayList<>();
public JRoot(JadxWrapper wrapper) {
this.wrapper = wrapper;
public JRoot(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.wrapper = mainWindow.getWrapper();
}
public final void update() {
removeAllChildren();
add(new JInputs(wrapper));
add(new JInputs(mainWindow));
add(new JSources(this, wrapper));
List<ResourceFile> resources = wrapper.getResources();
@@ -828,7 +828,7 @@ public class MainWindow extends JFrame {
}
public void initTree() {
treeRoot = new JRoot(wrapper);
treeRoot = new JRoot(this);
treeRoot.setFlatPackages(isFlattenPackage);
treeModel.setRoot(treeRoot);
addTreeCustomNodes();
@@ -3,13 +3,13 @@ package jadx.gui.utils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.formdev.flatlaf.extras.FlatSVGIcon;
import javax.swing.ImageIcon;
public class IconsCache {
private static final Map<String, FlatSVGIcon> SVG_ICONS = new ConcurrentHashMap<>();
private static final Map<String, ImageIcon> SVG_ICONS = new ConcurrentHashMap<>();
public static FlatSVGIcon getSVGIcon(String name) {
public static ImageIcon getSVGIcon(String name) {
return SVG_ICONS.computeIfAbsent(name, UiUtils::openSvgIcon);
}
}
@@ -88,7 +88,7 @@ public class UiUtils {
private UiUtils() {
}
public static FlatSVGIcon openSvgIcon(String name) {
public static ImageIcon openSvgIcon(String name) {
String iconPath = "icons/" + name + ".svg";
FlatSVGIcon icon = new FlatSVGIcon(iconPath);
boolean found;
@@ -0,0 +1,92 @@
package jadx.gui.utils.plugins;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.plugins.context.ITreeInputCategory;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
public class TreeInputsHelper {
private static final Logger LOG = LoggerFactory.getLogger(TreeInputsHelper.class);
private final List<CategoryData> categoryData;
private List<Path> simpleFiles;
public TreeInputsHelper(MainWindow mainWindow) {
categoryData = mainWindow.getWrapper().getGuiPluginsContext()
.getTreeInputCategories()
.stream()
.map(CategoryData::new)
.collect(Collectors.toList());
}
public void processInputs(List<Path> files) {
simpleFiles = new ArrayList<>(files.size());
for (Path file : files) {
boolean added = false;
for (CategoryData data : categoryData) {
if (data.filesFilter(file)) {
added = true;
break;
}
}
if (!added) {
simpleFiles.add(file);
}
}
}
public List<JNode> getCustomNodes() {
return categoryData.stream()
.filter(CategoryData::notEmpty)
.map(CategoryData::buildInputNode)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public List<Path> getSimpleFiles() {
return simpleFiles;
}
private static final class CategoryData {
private final ITreeInputCategory provider;
private final List<Path> collectedFiles = new ArrayList<>();
private CategoryData(ITreeInputCategory provider) {
this.provider = provider;
}
public boolean filesFilter(Path file) {
try {
if (provider.filesFilter(file)) {
collectedFiles.add(file);
return true;
}
} catch (Exception e) {
LOG.error("Failed to filter input files", e);
}
return false;
}
public @Nullable JNode buildInputNode() {
try {
return provider.buildInputNode(collectedFiles);
} catch (Exception e) {
LOG.error("Failed to build custom input node", e);
return null;
}
}
public boolean notEmpty() {
return !collectedFiles.isEmpty();
}
}
}
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -116,12 +117,25 @@ public class TestI18n {
fail("I18n file: " + path.getFileName() + " and " + DEFAULT_LANG_FILE + " differ in line " + line);
}
/**
* Temporary solution to allow use I18N strings in plugins until proper API implemented
*/
private static final List<String> EXCLUDED_KEYS = Arrays.asList(
// keys from `jadx-script-kotlin`
"tree.input_scripts",
"popup.new_script",
"popup.add_scripts",
"script.log",
"script.format",
"script.check");
@Test
public void keyIsUsed() throws IOException {
Properties properties = new Properties();
try (Reader reader = Files.newBufferedReader(i18nPath.resolve(DEFAULT_LANG_FILE))) {
properties.load(reader);
}
EXCLUDED_KEYS.forEach(properties.keySet()::remove);
Set<String> keys = new HashSet<>();
for (Object key : properties.keySet()) {
keys.add("\"" + key + '"');
@@ -26,6 +26,7 @@ import jadx.api.plugins.JadxPluginInfo;
import jadx.core.Jadx;
import jadx.core.plugins.versions.VerifyRequiredVersion;
import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.plugins.tools.data.JadxInstalledPlugins;
import jadx.plugins.tools.data.JadxPluginMetadata;
@@ -83,12 +84,13 @@ public class JadxPluginsTools {
install(pluginMetadata);
return pluginMetadata;
}
rejectedVersions.add(" version " + pluginMetadata.getVersion()
+ " not compatible, require: " + pluginMetadata.getRequiredJadxVersion());
String pluginVersion = Utils.getOrElse(pluginMetadata.getVersion(), "unknown");
rejectedVersions.add(" version '" + pluginVersion + "' not compatible, require: "
+ pluginMetadata.getRequiredJadxVersion());
}
throw new JadxRuntimeException("Can't find compatible version to install"
+ ", current jadx version: " + verifyRequiredVersion.getJadxVersion()
+ "\nrejected versions:\n"
+ "\nrejected plugin versions:\n"
+ String.join("\n", rejectedVersions));
}
@@ -6,7 +6,7 @@ plugins {
dependencies {
api(project(":jadx-core"))
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")
implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.10")
testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output)
testImplementation("org.apache.commons:commons-lang3:3.20.0")
+18
View File
@@ -0,0 +1,18 @@
## 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
@@ -0,0 +1,87 @@
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()
}
}
@@ -0,0 +1,37 @@
package jadx.plugins.script.kotlin
import jadx.api.plugins.JadxPlugin
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.JadxPluginInfo
import jadx.api.plugins.JadxPluginInfoBuilder
import jadx.plugins.script.kotlin.gui.JadxScriptInputCategory
import jadx.plugins.script.kotlin.gui.JadxScriptOptionsUI
import jadx.plugins.script.kotlin.passes.JadxScriptAfterLoadPass
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
class JadxScriptKotlinPlugin : JadxPlugin {
companion object {
const val PLUGIN_ID = "jadx-script-kotlin"
}
override fun getPluginInfo(): JadxPluginInfo = JadxPluginInfoBuilder.pluginId(PLUGIN_ID)
.name("Jadx Script (Kotlin)")
.description("Scripting support for jadx using Kotlin script")
.homepage("https://github.com/jadx-decompiler/jadx-script-kotlin")
.requiredJadxVersion("1.5.4, r2596")
.provides("jadx-script") // conflict with bundled plugin from older jadx versions
.build()
override fun init(context: JadxPluginContext) {
val scriptOptions = JadxScriptAllOptions()
context.registerOptions(scriptOptions)
val scripts = ScriptEval().process(context, scriptOptions)
if (scripts.isNotEmpty()) {
context.addPass(JadxScriptAfterLoadPass(scripts))
context.guiContext?.let { guiContext ->
JadxScriptOptionsUI.setup(guiContext, scriptOptions)
JadxScriptInputCategory.register(context, guiContext)
}
}
}
}
@@ -1,9 +1,11 @@
package jadx.plugins.script
package jadx.plugins.script.kotlin
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.api.plugins.JadxPluginContext
import jadx.core.utils.files.FileUtils
import java.io.File
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
@@ -12,6 +14,8 @@ 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)
@@ -19,7 +23,9 @@ class ScriptCache {
if (!enableCache) {
return CompiledJvmScriptsCache.NoCache
}
return JadxScriptsCache(getCacheDir(context))
val cacheDir = getCacheDir(context)
log.debug { "script cache created in : $cacheDir" }
return JadxScriptsCache(cacheDir)
}
/**
@@ -27,21 +33,22 @@ class ScriptCache {
* 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: File) : CompiledJvmScriptsCache {
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()) {
return null
}
return file.loadScriptFromJar() ?: run {
// invalidate cache if the script cannot be loaded
FileUtils.deleteDir(cacheDir)
null
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(
@@ -55,34 +62,35 @@ class ScriptCache {
val cacheDir = hashDir(baseCacheDir, script)
val file = hashFile(cacheDir, script, scriptCompilationConfiguration)
cacheDir.deleteRecursively()
cacheDir.mkdirs()
jvmScript.saveToJar(file)
FileUtils.deleteDirIfExists(cacheDir)
FileUtils.makeDirs(cacheDir)
jvmScript.saveToJar(file.toFile())
log.debug { "script cached: $file" }
}
}
private fun getCacheDir(context: JadxPluginContext): File {
val cacheBaseDir = context.files().pluginCacheDir.resolve("compiled").toFile()
private fun getCacheDir(context: JadxPluginContext): Path {
val cacheBaseDir = context.files().pluginCacheDir.resolve("compiled")
FileUtils.makeDirs(cacheBaseDir)
return cacheBaseDir
}
companion object {
private fun hashDir(baseCacheDir: File, script: SourceCode): File {
private fun hashDir(baseCacheDir: Path, script: SourceCode): Path {
if (script.name == null && script.locationId == null) {
return File(baseCacheDir, "tmp")
return baseCacheDir.resolve("tmp")
}
val digest = MessageDigest.getInstance("MD5")
digest.add(script.name)
digest.add(script.locationId)
return File(baseCacheDir, digest.digest().toHexString())
return baseCacheDir.resolve(digest.digest().toHexString())
}
private fun hashFile(
cacheDir: File,
cacheDir: Path,
script: SourceCode,
scriptCompilationConfiguration: ScriptCompilationConfiguration,
): File {
): Path {
val digest = MessageDigest.getInstance("MD5")
digest.add(script.text)
scriptCompilationConfiguration.notTransientData.entries
@@ -91,7 +99,7 @@ class ScriptCache {
digest.add(it.key.name)
digest.add(it.value.toString())
}
return File(cacheDir, digest.digest().toHexString() + ".jar")
return cacheDir.resolve(digest.digest().toHexString() + ".jar")
}
private fun MessageDigest.add(str: String?) {
@@ -0,0 +1,212 @@
package jadx.plugins.script.kotlin
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.api.plugins.JadxPluginContext
import jadx.plugins.script.kotlin.runtime.JadxScriptData
import jadx.plugins.script.kotlin.runtime.JadxScriptTemplate
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.scripting.resolve.skipExtensionsResolutionForImplicitsExceptInnermost
import java.io.File
import kotlin.script.experimental.api.EvaluationResult
import kotlin.script.experimental.api.KotlinType
import kotlin.script.experimental.api.ResultValue
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptAcceptedLocation
import kotlin.script.experimental.api.ScriptCollectedData
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptConfigurationRefinementContext
import kotlin.script.experimental.api.ScriptDiagnostic.Severity
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
import kotlin.script.experimental.api.acceptedLocations
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.api.collectedAnnotations
import kotlin.script.experimental.api.compilationConfiguration
import kotlin.script.experimental.api.compilerOptions
import kotlin.script.experimental.api.constructorArgs
import kotlin.script.experimental.api.defaultIdentifier
import kotlin.script.experimental.api.defaultImports
import kotlin.script.experimental.api.displayName
import kotlin.script.experimental.api.fileExtension
import kotlin.script.experimental.api.filePathPattern
import kotlin.script.experimental.api.hostConfiguration
import kotlin.script.experimental.api.ide
import kotlin.script.experimental.api.implicitReceivers
import kotlin.script.experimental.api.isStandalone
import kotlin.script.experimental.api.onSuccess
import kotlin.script.experimental.api.refineConfiguration
import kotlin.script.experimental.api.with
import kotlin.script.experimental.dependencies.CompoundDependenciesResolver
import kotlin.script.experimental.dependencies.DependsOn
import kotlin.script.experimental.dependencies.FileSystemDependenciesResolver
import kotlin.script.experimental.dependencies.Repository
import kotlin.script.experimental.dependencies.maven.MavenDependenciesResolver
import kotlin.script.experimental.dependencies.resolveFromScriptSourceAnnotations
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.host.getScriptingClass
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.host.with
import kotlin.script.experimental.jvm.JvmGetScriptingClass
import kotlin.script.experimental.jvm.baseClassLoader
import kotlin.script.experimental.jvm.compilationCache
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
import kotlin.script.experimental.jvm.jvm
import kotlin.script.experimental.jvm.updateClasspath
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
import kotlin.system.measureTimeMillis
import kotlin.time.DurationUnit
import kotlin.time.toDuration
private val log = KotlinLogging.logger {}
object DefCompileConf : ScriptCompilationConfiguration(ScriptEval.buildDefaultCompileConf())
class ScriptEval {
companion object {
fun buildDefaultCompileConf(): ScriptCompilationConfiguration {
val scriptEval = ScriptEval()
val hostConf = scriptEval.buildHostConf(null)
return scriptEval.buildCompileConf(hostConf)
}
}
fun process(context: JadxPluginContext, scriptOptions: JadxScriptAllOptions): List<JadxScriptData> {
val jadx = context.decompiler
val scripts = jadx.args.inputFiles.filter { f -> f.name.endsWith(".jadx.kts") }
if (scripts.isEmpty()) {
return emptyList()
}
val scriptDataList = mutableListOf<JadxScriptData>()
for (scriptFile in scripts) {
val scriptData = JadxScriptData(jadx, context, scriptOptions, scriptFile)
scriptDataList.add(scriptData)
eval(context, scriptData)
}
return scriptDataList
}
private fun eval(
context: JadxPluginContext,
scriptData: JadxScriptData,
) {
scriptData.log.debug { "Loading script: ${scriptData.scriptFile.absolutePath}" }
val hostConf = buildHostConf(context)
val compileConf = buildCompileConf(hostConf)
val evalConf = buildEvalConf(scriptData, compileConf)
val scriptingHost = BasicJvmScriptingHost(hostConf)
val execTime = measureTimeMillis {
val result = scriptingHost.eval(scriptData.scriptFile.toScriptSource(), compileConf, evalConf)
processEvalResult(result, scriptData)
}
scriptData.log.debug { "Script '${scriptData.scriptName}' executed in ${execTime.toDuration(DurationUnit.MILLISECONDS)}" }
}
private fun processEvalResult(res: ResultWithDiagnostics<EvaluationResult>, scriptData: JadxScriptData) {
val log = scriptData.log
for (r in res.reports) {
val msg = r.render(withSeverity = false)
when (r.severity) {
Severity.FATAL, Severity.ERROR -> log.error(r.exception) { "Script execution error: $msg" }
Severity.WARNING -> log.warn { "Script execution issue: $msg" }
Severity.INFO -> log.info { "Script report: $msg" }
Severity.DEBUG -> log.debug { "Script debug: $msg" }
}
}
when (res) {
is ResultWithDiagnostics.Success -> {
when (val retVal = res.value.returnValue) {
is ResultValue.Error -> log.error(retVal.error) { "Script execution error:" }
is ResultValue.Value -> log.info { "Script execution result: $retVal" }
is ResultValue.Unit -> {}
ResultValue.NotEvaluated -> {}
}
}
is ResultWithDiagnostics.Failure -> {
scriptData.error = true
log.error { "Script execution failed: ${scriptData.scriptName}" }
}
}
}
fun buildHostConf(context: JadxPluginContext?) = ScriptingHostConfiguration {
jvm {
getScriptingClass(JvmGetScriptingClass())
baseClassLoader.put(JadxScriptTemplate::class.java.classLoader)
context?.let {
compilationCache(ScriptCache().build(context))
}
}
}
fun buildCompileConf(scriptingHostConf: ScriptingHostConfiguration) = ScriptCompilationConfiguration {
hostConfiguration.put(scriptingHostConf)
displayName.put("Jadx script")
defaultIdentifier.put("JadxScript")
fileExtension.put("jadx.kts")
filePathPattern.put(".*\\.jadx\\.kts")
val receiversTypes = listOf(KotlinType(JadxScriptTemplate::class))
implicitReceivers(receiversTypes)
skipExtensionsResolutionForImplicitsExceptInnermost(receiversTypes)
jvm {
dependenciesFromCurrentContext(
wholeClasspath = true,
)
}
addBaseClass<JadxScriptTemplate>()
defaultImports(DependsOn::class, Repository::class)
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
ide {
acceptedLocations(ScriptAcceptedLocation.Everywhere)
}
isStandalone(true)
// forcing compiler to not use modules while building script classpath
// because shadow jar remove all modules-info.class (https://github.com/GradleUp/shadow/issues/710)
compilerOptions.append("-Xjdk-release=1.8")
}
inline fun <reified T> ScriptCompilationConfiguration.Builder.addBaseClass() {
val kClass = T::class
defaultImports.append(kClass.java.name)
hostConfiguration.update {
it.with {
this[jvm.baseClassLoader] = kClass.java.classLoader
}
}
}
fun buildEvalConf(scriptData: JadxScriptData, compileConf: ScriptCompilationConfiguration): ScriptEvaluationConfiguration {
return ScriptEvaluationConfiguration {
hostConfiguration.put(compileConf[hostConfiguration]!!)
compilationConfiguration.put(compileConf)
constructorArgs(JadxScriptTemplate(scriptData))
}
}
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)
?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess()
return runBlocking {
resolver.resolveFromScriptSourceAnnotations(annotations)
}.onSuccess { files: List<File> ->
log.debug { "add script dependency: $files" }
context.compilationConfiguration.with {
updateClasspath(files)
}.asSuccess()
}
}
}
@@ -1,27 +1,27 @@
package jadx.plugins.script.ide
package jadx.plugins.script.kotlin
import jadx.plugins.script.ScriptEval
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.SourceCode
import kotlin.script.experimental.api.SourceCodeCompletionVariant
import kotlin.script.experimental.api.analysisDiagnostics
import kotlin.script.experimental.api.hostConfiguration
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.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.host.with
import kotlin.script.experimental.jvm.util.isError
import kotlin.script.experimental.jvm.util.toSourceCodePosition
const val AUTO_COMPLETE_INSERT_STR = "ABCDEF" // defined at KJvmReplCompleter.INSERTED_STRING
data class ScriptCompletionResult(
val completions: List<SourceCodeCompletionVariant>,
val reports: List<ScriptDiagnostic>,
val reports: MutableList<ScriptDiagnostic>,
)
data class ScriptAnalyzeResult(
@@ -30,12 +30,28 @@ data class ScriptAnalyzeResult(
val renderType: String?,
)
class ScriptServices {
private val compileConf = ScriptEval().buildCompileConf()
private val replCompiler = KJvmReplCompilerWithIdeServices(
compileConf[ScriptCompilationConfiguration.hostConfiguration]
?: defaultJvmScriptingHostConfiguration,
)
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)
@@ -44,15 +60,14 @@ class ScriptServices {
}
return ScriptCompletionResult(
completions = result.valueOrNull()?.toList() ?: emptyList(),
reports = result.reports,
reports = result.reports.toMutableList(),
)
}
fun analyze(scriptName: String, code: String): ScriptAnalyzeResult {
val sourceCode = code.toScriptSource(scriptName)
val result = runBlocking {
val cursor = SourceCode.Position(0, 0) // not used
replCompiler.analyze(sourceCode, cursor, compileConf)
replCompiler.analyze(sourceCode, 0.toSourceCodePosition(sourceCode), compileConf)
}
val analyzerResult = result.valueOrNull()
val issues = mutableListOf<ScriptDiagnostic>()
@@ -0,0 +1,92 @@
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()
}
}
@@ -0,0 +1,51 @@
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")
}
}
@@ -0,0 +1,44 @@
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) }
}
}
@@ -0,0 +1,30 @@
package jadx.plugins.script.kotlin.gui
import jadx.api.plugins.gui.ISettingsGroup
import jadx.api.plugins.gui.JadxGuiContext
import jadx.plugins.script.kotlin.runtime.data.JadxScriptAllOptions
import javax.swing.JPanel
object JadxScriptOptionsUI {
fun setup(guiContext: JadxGuiContext, scriptOptions: JadxScriptAllOptions) {
guiContext.settings().setCustomSettingsGroup(ScriptOptionsRootGroup(guiContext, scriptOptions))
}
}
private class ScriptOptionsRootGroup(
private val guiContext: JadxGuiContext,
private val scriptOptions: JadxScriptAllOptions,
) : ISettingsGroup {
override fun getTitle() = "Scripts"
override fun buildComponent() = JPanel() // empty panel for root node
override fun getSubGroups(): List<ISettingsGroup> {
val settings = guiContext.settings()
return scriptOptions.descriptions
.groupBy { it.script }
.map { (script, options) -> settings.buildSettingsGroupForOptions(script, options) }
.toList()
}
}
@@ -1,4 +1,4 @@
package jadx.gui.plugins.script
package jadx.plugins.script.kotlin.gui
import com.pinterest.ktlint.rule.engine.api.Code
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
@@ -9,13 +9,15 @@ 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
import org.slf4j.Logger
import org.slf4j.LoggerFactory
data class JadxLintError(
val line: Int,
val col: Int,
val ruleId: String,
val detail: String,
)
object KtLintUtils {
val LOG: Logger = LoggerFactory.getLogger(KtLintUtils::class.java)
private val ktLint by lazy {
KtLintRuleEngine(
ruleProviders = StandardRuleSetProvider().getRuleProviders(),
@@ -36,18 +38,18 @@ object KtLintUtils {
}
fun lint(content: String): List<JadxLintError> {
val code = Code.fromSnippet(content, script = true)
val errors = mutableListOf<JadxLintError>()
val code = Code.fromSnippet(content, script = true)
ktLint.lint(code) { lintError ->
errors.add(JadxLintError(lintError.line, lintError.col, lintError.ruleId.value, lintError.detail))
errors.add(
JadxLintError(
line = lintError.line,
col = lintError.col,
ruleId = lintError.ruleId.value,
detail = lintError.detail,
),
)
}
return errors
}
}
data class JadxLintError(
val line: Int,
val col: Int,
val ruleId: String,
val detail: String,
)
@@ -0,0 +1,76 @@
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()
}
}
@@ -0,0 +1,137 @@
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
}
}
@@ -0,0 +1,58 @@
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
}
}
@@ -0,0 +1,26 @@
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)))),
)
}
}
@@ -0,0 +1,251 @@
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()
}
}
@@ -0,0 +1,108 @@
package jadx.plugins.script.kotlin.gui
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument
import org.fife.ui.rsyntaxtextarea.parser.AbstractParser
import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult
import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice
import org.fife.ui.rsyntaxtextarea.parser.ParseResult
import org.fife.ui.rsyntaxtextarea.parser.ParserNotice
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import kotlin.script.experimental.api.ScriptDiagnostic
class ScriptErrorService(private val scriptArea: ScriptCodeArea) : AbstractParser() {
private val result: DefaultParseResult = DefaultParseResult(this)
override fun parse(doc: RSyntaxDocument?, style: String?): ParseResult {
return result
}
fun clearErrors() {
result.clearNotices()
scriptArea.removeParser(this)
}
fun apply() {
scriptArea.removeParser(this)
scriptArea.addParser(this)
scriptArea.addNotify()
scriptArea.requestFocus()
jumpCaretToFirstError()
}
private fun jumpCaretToFirstError() {
val parserNotices = result.notices
if (parserNotices.isEmpty()) {
return
}
val notice = parserNotices.get(0)
var offset = notice.offset
if (offset == -1) {
try {
offset = scriptArea.getLineStartOffset(notice.line)
} catch (e: Exception) {
LOG.error("Failed to jump to first error", e)
return
}
}
scriptArea.scrollToPos(offset)
}
fun addCompilerIssues(issues: List<ScriptDiagnostic>) {
for (issue in issues) {
if (issue.severity == ScriptDiagnostic.Severity.DEBUG) {
continue
}
val notice: DefaultParserNotice?
val loc = issue.location
if (loc == null) {
notice = DefaultParserNotice(this, issue.message, 0)
} else {
try {
val line = loc.start.line
val offset = scriptArea.getLineStartOffset(line - 1) + loc.start.col
val len = if (loc.end == null) -1 else loc.end!!.col - loc.start.col
notice = DefaultParserNotice(this, issue.message, line, offset - 1, len)
notice.setLevel(convertLevel(issue.severity))
} catch (e: Exception) {
LOG.error("Failed to convert script issue", e)
continue
}
}
addNotice(notice)
}
}
fun addLintErrors(errors: List<JadxLintError>) {
for (error in errors) {
try {
val line = error.line
val offset = scriptArea.getLineStartOffset(line - 1) + error.col - 1
val word = scriptArea.getWordByPosition(offset)
val len = word?.length ?: -1
val notice = DefaultParserNotice(this, error.detail, line, offset, len)
notice.setLevel(ParserNotice.Level.WARNING)
addNotice(notice)
} catch (e: Exception) {
LOG.error("Failed to convert lint error", e)
}
}
}
private fun addNotice(notice: DefaultParserNotice) {
LOG.debug("Add notice: {}:{}:{} - {}", notice.line, notice.offset, notice.length, notice.message)
result.addNotice(notice)
}
companion object {
private val LOG: Logger = LoggerFactory.getLogger(ScriptErrorService::class.java)
private fun convertLevel(severity: ScriptDiagnostic.Severity): ParserNotice.Level {
return when (severity) {
ScriptDiagnostic.Severity.FATAL, ScriptDiagnostic.Severity.ERROR -> ParserNotice.Level.ERROR
ScriptDiagnostic.Severity.WARNING -> ParserNotice.Level.WARNING
ScriptDiagnostic.Severity.INFO, ScriptDiagnostic.Severity.DEBUG -> ParserNotice.Level.INFO
}
}
}
}
@@ -1,9 +1,9 @@
package jadx.plugins.script.passes
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.runtime.JadxScriptData
import jadx.plugins.script.kotlin.runtime.JadxScriptData
class JadxScriptAfterLoadPass(private val scripts: List<JadxScriptData>) : JadxAfterLoadPass {
@@ -0,0 +1,29 @@
package jadx.plugins.script.kotlin.runtime
import jadx.plugins.script.kotlin.DefCompileConf
import kotlin.script.experimental.annotations.KotlinScript
@KotlinScript(
displayName = "Jadx Script",
fileExtension = "jadx.kts",
filePathPattern = ".*\\.jadx\\.kts",
compilationConfiguration = DefCompileConf::class,
)
open class JadxScriptTemplate(
scriptData: JadxScriptData,
) {
val scriptName = scriptData.scriptName
val log = scriptData.log
private val scriptInstance = JadxScriptInstance(scriptData, log)
fun getJadxInstance() = scriptInstance
fun println(message: Any?) {
log.info { message }
}
fun print(message: Any?) {
log.info { message }
}
}
@@ -1,7 +1,7 @@
@file:JvmName("ScriptRuntime")
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package jadx.plugins.script.runtime
package jadx.plugins.script.kotlin.runtime
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
@@ -11,29 +11,30 @@ 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.runtime.data.Debug
import jadx.plugins.script.runtime.data.Decompile
import jadx.plugins.script.runtime.data.Gui
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
import jadx.plugins.script.runtime.data.JadxScriptOptions
import jadx.plugins.script.runtime.data.Rename
import jadx.plugins.script.runtime.data.Replace
import jadx.plugins.script.runtime.data.Search
import jadx.plugins.script.runtime.data.Stages
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
const val JADX_SCRIPT_LOG_PREFIX = "JadxScript:"
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: MutableList<() -> Unit> = ArrayList()
val afterLoad = mutableListOf<() -> Unit>()
var error: Boolean = false
}
@@ -1,9 +1,9 @@
package jadx.plugins.script.runtime.data
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.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
import java.io.File
class Debug(private val jadx: JadxScriptInstance) {
@@ -1,14 +1,18 @@
package jadx.plugins.script.runtime.data
package jadx.plugins.script.kotlin.runtime.data
import jadx.api.JadxArgs
import jadx.api.JavaClass
import jadx.plugins.script.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
import java.util.concurrent.Executors
class Decompile(private val jadx: JadxScriptInstance) {
fun all() {
jadx.classes.forEach(JavaClass::decompile)
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) {
@@ -1,8 +1,8 @@
package jadx.plugins.script.runtime.data
package jadx.plugins.script.kotlin.runtime.data
import jadx.api.metadata.ICodeNodeRef
import jadx.api.plugins.gui.JadxGuiContext
import jadx.plugins.script.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Gui(
private val jadx: JadxScriptInstance,
@@ -1,11 +1,11 @@
package jadx.plugins.script.runtime.data
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.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class JadxScriptAllOptions : JadxPluginOptions {
lateinit var values: Map<String, String>
@@ -1,10 +1,10 @@
package jadx.plugins.script.runtime.data
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.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Rename(private val jadx: JadxScriptInstance) {
@@ -1,11 +1,11 @@
package jadx.plugins.script.runtime.data
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.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Replace(private val jadx: JadxScriptInstance) {
@@ -1,7 +1,7 @@
package jadx.plugins.script.runtime.data
package jadx.plugins.script.kotlin.runtime.data
import jadx.core.dex.nodes.ClassNode
import jadx.plugins.script.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Search(jadx: JadxScriptInstance) {
private val dec = jadx.internalDecompiler
@@ -1,11 +1,11 @@
package jadx.plugins.script.runtime.data
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.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
class Stages(private val jadx: JadxScriptInstance) {
@@ -1,4 +1,4 @@
package jadx.plugins.script.runtime.data
package jadx.plugins.script.kotlin.runtime.data
import jadx.api.plugins.pass.JadxPass
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo
@@ -8,7 +8,7 @@ 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.runtime.JadxScriptInstance
import jadx.plugins.script.kotlin.runtime.JadxScriptInstance
private fun buildScriptName(jadx: JadxScriptInstance, name: String) = "JadxScript$name(${jadx.scriptName})"
@@ -0,0 +1 @@
jadx.plugins.script.kotlin.JadxScriptKotlinPlugin
@@ -1,7 +1,10 @@
package jadx.plugins.script.ide
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 {
@@ -12,8 +15,10 @@ class ScriptServicesTest {
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"
@@ -21,6 +26,7 @@ class ScriptServicesTest {
val result = ScriptServices().analyze(name, script)
println(result)
assertThat(result.success).isTrue()
assertThat(result.issues).noneMatch { it.severity == ERROR }
}
@Test
@@ -38,6 +44,22 @@ class ScriptServicesTest {
.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()
-29
View File
@@ -1,29 +0,0 @@
## JADX scripting support
:exclamation: Work still in progress! Script API is not stable!
### Examples
Check script examples in [`examples/scripts/`](https://github.com/skylot/jadx/tree/master/jadx-plugins/jadx-script/examples/scripts)(start with [`hello`](https://github.com/skylot/jadx/blob/master/jadx-plugins/jadx-script/examples/scripts/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
### Script development
Jadx-gui for now don't support ~~autocompletion~~, ~~errors highlighting~~, code navigation and docs,
so the best approach for script editing is to open jadx project in IntelliJ IDEA and write your script in `examples/scripts/` folder.
Also, this allows to debug your scripts: for that you need to create run configuration for jadx-cli or jadx-gui
add breakpoints and next run it in debug mode (jadx-gui is preferred because of faster script reload).
Script logs and compilation errors will appear in `Log viewer` (try filter for show only script related logs)
@@ -1,30 +0,0 @@
plugins {
id("jadx-kotlin")
}
dependencies {
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
implementation(kotlin("stdlib-common"))
implementation(kotlin("script-runtime"))
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
// script context support in IDE is poor, use stubs and manual imports for now
// kotlinScriptDef(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
// 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.15.0")
}
sourceSets {
main {
kotlin.srcDirs(
"scripts",
"scripts/deobf",
"scripts/gui",
"context",
)
}
}
@@ -1,28 +0,0 @@
@file:Suppress("MayBeConstant", "unused")
import io.github.oshai.kotlinlogging.KotlinLogging
import jadx.plugins.script.runtime.JadxScriptInstance
/**
* Stubs for JadxScriptBaseClass script super class
*/
val log = KotlinLogging.logger("JadxScript")
val scriptName = "script"
fun getJadxInstance(): JadxScriptInstance {
throw IllegalStateException("Stub method!")
}
/**
* Annotations for maven imports
*/
@Target(AnnotationTarget.FILE)
@Repeatable
@Retention(AnnotationRetention.SOURCE)
annotation class DependsOn(vararg val artifactsCoordinates: String, val options: Array<String> = [])
@Target(AnnotationTarget.FILE)
@Repeatable
@Retention(AnnotationRetention.SOURCE)
annotation class Repository(vararg val repositoriesCoordinates: String, val options: Array<String> = [])
@@ -1,17 +0,0 @@
plugins {
id("jadx-kotlin")
id("jadx-library")
}
dependencies {
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
implementation(project(":jadx-plugins:jadx-script:jadx-script-plugin"))
implementation(kotlin("scripting-common"))
implementation(kotlin("scripting-jvm"))
implementation(kotlin("scripting-compiler-embeddable"))
implementation(kotlin("scripting-ide-services"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
}
@@ -1,17 +0,0 @@
plugins {
id("jadx-library")
id("jadx-kotlin")
}
dependencies {
implementation(project(":jadx-plugins:jadx-script:jadx-script-runtime"))
implementation(project(":jadx-commons:jadx-app-commons"))
implementation(kotlin("scripting-common"))
implementation(kotlin("scripting-jvm"))
implementation(kotlin("scripting-jvm-host"))
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
testImplementation(project(":jadx-core"))
}
@@ -1,30 +0,0 @@
package jadx.plugins.script
import jadx.api.plugins.gui.ISettingsGroup
import jadx.api.plugins.gui.JadxGuiContext
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
import javax.swing.JPanel
object JadxScriptOptionsUI {
fun setup(guiContext: JadxGuiContext, scriptOptions: JadxScriptAllOptions) {
val settings = guiContext.settings()
val subGroups = scriptOptions.descriptions
.groupBy { it.script }
.map { (script, options) -> settings.buildSettingsGroupForOptions(script, options) }
.toList()
settings.setCustomSettingsGroup(EmptyRootGroup("Scripts", subGroups))
}
}
private class EmptyRootGroup(
private val title: String,
private val subGroups: List<ISettingsGroup>,
) : ISettingsGroup {
override fun getTitle() = title
override fun buildComponent() = JPanel()
override fun getSubGroups() = subGroups
}
@@ -1,21 +0,0 @@
package jadx.plugins.script
import jadx.api.plugins.JadxPlugin
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.JadxPluginInfo
import jadx.plugins.script.passes.JadxScriptAfterLoadPass
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
class JadxScriptPlugin : JadxPlugin {
override fun getPluginInfo() = JadxPluginInfo("jadx-script", "Jadx Script", "Scripting support for jadx")
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 { JadxScriptOptionsUI.setup(it, scriptOptions) }
}
}
}
@@ -1,102 +0,0 @@
package jadx.plugins.script
import jadx.api.plugins.JadxPluginContext
import jadx.plugins.script.runtime.JadxScriptData
import jadx.plugins.script.runtime.JadxScriptTemplate
import jadx.plugins.script.runtime.data.JadxScriptAllOptions
import kotlin.script.experimental.api.EvaluationResult
import kotlin.script.experimental.api.ResultValue
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptDiagnostic.Severity
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
import kotlin.script.experimental.api.constructorArgs
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.host.toScriptSource
import kotlin.script.experimental.jvm.compilationCache
import kotlin.script.experimental.jvm.jvm
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate
import kotlin.script.experimental.jvmhost.createJvmEvaluationConfigurationFromTemplate
import kotlin.system.measureTimeMillis
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class ScriptEval {
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 scriptingHost = buildScriptingHost(context)
val compileConf = buildCompileConf()
val scriptDataList = mutableListOf<JadxScriptData>()
for (scriptFile in scripts) {
val scriptData = JadxScriptData(jadx, context, scriptOptions, scriptFile)
scriptDataList.add(scriptData)
eval(scriptingHost, compileConf, scriptData)
}
return scriptDataList
}
private fun eval(
scriptingHost: BasicJvmScriptingHost,
compileConf: ScriptCompilationConfiguration,
scriptData: JadxScriptData,
) {
scriptData.log.debug { "Loading script: ${scriptData.scriptFile.absolutePath}" }
val evalConf = buildEvalConf(scriptData)
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 -> {} // ignore, too verbose
}
}
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 buildScriptingHost(context: JadxPluginContext) = BasicJvmScriptingHost(
baseHostConfiguration = ScriptingHostConfiguration {
jvm {
compilationCache(ScriptCache().build(context))
}
},
)
fun buildCompileConf() = createJvmCompilationConfigurationFromTemplate<JadxScriptTemplate>()
fun buildEvalConf(scriptData: JadxScriptData): ScriptEvaluationConfiguration {
val baseEvalConf = createJvmEvaluationConfigurationFromTemplate<JadxScriptTemplate>()
return ScriptEvaluationConfiguration(baseEvalConf) {
constructorArgs(scriptData)
}
}
}
@@ -1 +0,0 @@
jadx.plugins.script.JadxScriptPlugin
@@ -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,25 +0,0 @@
plugins {
id("jadx-library")
id("jadx-kotlin")
}
dependencies {
api(project(":jadx-core"))
implementation(kotlin("stdlib"))
implementation(kotlin("scripting-common"))
implementation(kotlin("scripting-jvm"))
// allow to use maven dependencies in scripts
implementation(kotlin("scripting-dependencies"))
implementation(kotlin("scripting-dependencies-maven"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("io.github.oshai:kotlin-logging-jvm:7.0.13")
runtimeOnly(project(":jadx-plugins:jadx-dex-input"))
runtimeOnly(project(":jadx-plugins:jadx-smali-input"))
runtimeOnly(project(":jadx-plugins:jadx-java-convert"))
runtimeOnly(project(":jadx-plugins:jadx-java-input"))
runtimeOnly(project(":jadx-plugins:jadx-raung-input"))
}
@@ -1,91 +0,0 @@
package jadx.plugins.script.runtime
import kotlinx.coroutines.runBlocking
import kotlin.script.experimental.annotations.KotlinScript
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.acceptedLocations
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.api.collectedAnnotations
import kotlin.script.experimental.api.compilerOptions
import kotlin.script.experimental.api.defaultImports
import kotlin.script.experimental.api.dependencies
import kotlin.script.experimental.api.ide
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.jvm.JvmDependency
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
import kotlin.script.experimental.jvm.jvm
@KotlinScript(
displayName = "Jadx Script",
fileExtension = "jadx.kts",
compilationConfiguration = JadxScriptConfiguration::class,
)
abstract 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 }
}
}
object JadxScriptConfiguration : ScriptCompilationConfiguration({
defaultImports(DependsOn::class, Repository::class)
jvm {
dependenciesFromCurrentContext(
wholeClasspath = true,
)
}
ide {
acceptedLocations(ScriptAcceptedLocation.Everywhere)
}
refineConfiguration {
onAnnotations(DependsOn::class, Repository::class, handler = ::configureMavenDepsOnAnnotations)
}
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")
})
private val resolver = CompoundDependenciesResolver(FileSystemDependenciesResolver(), MavenDependenciesResolver())
fun configureMavenDepsOnAnnotations(context: ScriptConfigurationRefinementContext): ResultWithDiagnostics<ScriptCompilationConfiguration> {
val annotations = context.collectedData?.get(ScriptCollectedData.collectedAnnotations)
?.takeIf { it.isNotEmpty() }
?: return context.compilationConfiguration.asSuccess()
return runBlocking {
resolver.resolveFromScriptSourceAnnotations(annotations)
}.onSuccess {
context.compilationConfiguration.with {
dependencies.append(JvmDependency(it))
}.asSuccess()
}
}

Some files were not shown because too many files have changed in this diff Show More