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
+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,53 +0,0 @@
package jadx.gui.plugins.script
import com.pinterest.ktlint.rule.engine.api.Code
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine
import com.pinterest.ktlint.rule.engine.core.api.AutocorrectDecision
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider
import org.ec4j.core.model.PropertyType
import org.slf4j.Logger
import org.slf4j.LoggerFactory
object KtLintUtils {
val LOG: Logger = LoggerFactory.getLogger(KtLintUtils::class.java)
private val ktLint by lazy {
KtLintRuleEngine(
ruleProviders = StandardRuleSetProvider().getRuleProviders(),
editorConfigOverride = EditorConfigOverride.from(
CODE_STYLE_PROPERTY to CodeStyleValue.intellij_idea,
INDENT_STYLE_PROPERTY to PropertyType.IndentStyleValue.tab,
),
)
}
fun format(content: String): String {
val code = Code.fromSnippet(content, script = true)
return ktLint.format(
code,
rerunAfterAutocorrect = true,
defaultAutocorrect = true,
) { AutocorrectDecision.ALLOW_AUTOCORRECT }
}
fun lint(content: String): List<JadxLintError> {
val code = Code.fromSnippet(content, script = true)
val errors = mutableListOf<JadxLintError>()
ktLint.lint(code) { lintError ->
errors.add(JadxLintError(lintError.line, lintError.col, lintError.ruleId.value, lintError.detail))
}
return errors
}
}
data class JadxLintError(
val line: Int,
val col: Int,
val ruleId: String,
val detail: String,
)
@@ -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 + '"');