feat(gui): load available plugins from jadx-plugins-list

This commit is contained in:
Skylot
2023-08-05 22:41:51 +01:00
parent 8d26fa2a89
commit f213082da5
38 changed files with 678 additions and 284 deletions
@@ -4,20 +4,26 @@ public class JadxPluginInfo {
private final String pluginId;
private final String name;
private final String description;
private final String homepage;
/**
* Conflicting plugins should have same 'provides' property, only one will be loaded
* Conflicting plugins should have the same 'provides' property; only one will be loaded
*/
private final String provides;
public JadxPluginInfo(String id, String name, String description) {
this(id, name, description, id);
this(id, name, description, "", id);
}
public JadxPluginInfo(String pluginId, String name, String description, String provides) {
this(pluginId, name, description, "", provides);
}
public JadxPluginInfo(String pluginId, String name, String description, String homepage, String provides) {
this.pluginId = pluginId;
this.name = name;
this.description = description;
this.homepage = homepage;
this.provides = provides;
}
@@ -33,6 +39,10 @@ public class JadxPluginInfo {
return description;
}
public String getHomepage() {
return homepage;
}
public String getProvides() {
return provides;
}
@@ -0,0 +1,51 @@
package jadx.api.plugins;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
public class JadxPluginInfoBuilder {
private String pluginId;
private String name;
private String description;
private String homepage = "";
private @Nullable String provides;
public JadxPluginInfoBuilder() {
}
public JadxPluginInfoBuilder pluginId(String pluginId) {
this.pluginId = Objects.requireNonNull(pluginId);
return this;
}
public JadxPluginInfoBuilder name(String name) {
this.name = Objects.requireNonNull(name);
return this;
}
public JadxPluginInfoBuilder description(String description) {
this.description = Objects.requireNonNull(description);
return this;
}
public JadxPluginInfoBuilder homepage(String homepage) {
this.homepage = homepage;
return this;
}
public JadxPluginInfoBuilder provides(String provides) {
this.provides = provides;
return this;
}
public JadxPluginInfo build() {
Objects.requireNonNull(pluginId, "PluginId is required");
Objects.requireNonNull(name, "Name is required");
Objects.requireNonNull(description, "Description is required");
if (provides == null) {
provides = pluginId;
}
return new JadxPluginInfo(pluginId, name, description, homepage, provides);
}
}
@@ -1,6 +1,7 @@
package jadx.api.plugins.events;
import jadx.api.plugins.events.types.NodeRenamedByUser;
import jadx.api.plugins.events.types.ReloadProject;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.api.plugins.gui.JadxGuiSettings;
@@ -13,13 +14,18 @@ import static jadx.api.plugins.events.JadxEventType.create;
public class JadxEvents {
/**
* Notify about renames done by user (GUI only).
* Notify about renaming done by user (GUI only).
*/
public static final JadxEventType<NodeRenamedByUser> NODE_RENAMED_BY_USER = create();
/**
* Request reload of settings window (GUI only).
* Useful for reload custom settings group set with
* Request reload of a current project (GUI only).
*/
public static final JadxEventType<ReloadProject> RELOAD_PROJECT = create();
/**
* Request reload of a settings window (GUI only).
* Useful for a reload custom settings group which was set with
* {@link JadxGuiSettings#setCustomSettingsGroup(ISettingsGroup)}.
*/
public static final JadxEventType<ReloadSettingsWindow> RELOAD_SETTINGS_WINDOW = create();
@@ -0,0 +1,24 @@
package jadx.api.plugins.events.types;
import jadx.api.plugins.events.IJadxEvent;
import jadx.api.plugins.events.JadxEventType;
import jadx.api.plugins.events.JadxEvents;
public class ReloadProject implements IJadxEvent {
public static final ReloadProject INSTANCE = new ReloadProject();
private ReloadProject() {
// singleton
}
@Override
public JadxEventType<ReloadProject> getType() {
return JadxEvents.RELOAD_PROJECT;
}
@Override
public String toString() {
return "RELOAD_PROJECT";
}
}
@@ -6,7 +6,7 @@ import jadx.api.plugins.gui.ISettingsGroup;
import jadx.api.plugins.gui.JadxGuiSettings;
import jadx.api.plugins.options.OptionDescription;
import jadx.gui.settings.ui.SubSettingsGroup;
import jadx.gui.settings.ui.plugins.PluginsSettings;
import jadx.gui.settings.ui.plugins.PluginSettings;
import jadx.gui.ui.MainWindow;
public class GuiSettingsContext implements JadxGuiSettings {
@@ -24,7 +24,7 @@ public class GuiSettingsContext implements JadxGuiSettings {
@Override
public ISettingsGroup buildSettingsGroupForOptions(String title, List<OptionDescription> options) {
MainWindow mainWindow = guiPluginContext.getCommonContext().getMainWindow();
PluginsSettings pluginsSettings = new PluginsSettings(mainWindow, mainWindow.getSettings());
PluginSettings pluginsSettings = new PluginSettings(mainWindow, mainWindow.getSettings());
SubSettingsGroup settingsGroup = new SubSettingsGroup(title);
pluginsSettings.addOptions(settingsGroup, options);
return settingsGroup;
@@ -203,7 +203,7 @@ public class QuarkManager {
}
private void runCommand(List<String> cmd) throws Exception {
UiUtils.uiRun(() -> mainWindow.showLogViewer(LogOptions.forLevel(Level.INFO)));
mainWindow.showLogViewer(LogOptions.forLevel(Level.INFO));
LOG.info("Running command: {}", String.join(" ", cmd));
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.redirectErrorStream(true);
@@ -37,7 +37,6 @@ import javax.swing.ScrollPaneConstants;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.tree.TreePath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -61,7 +60,7 @@ import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsAdapter;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.settings.ui.cache.CacheSettingsGroup;
import jadx.gui.settings.ui.plugins.PluginsSettings;
import jadx.gui.settings.ui.plugins.PluginSettings;
import jadx.gui.settings.ui.shortcut.ShortcutsSettingsGroup;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.EditorTheme;
@@ -85,7 +84,7 @@ public class JadxSettingsWindow extends JDialog {
private final transient LangLocale prevLang;
private transient boolean needReload = false;
private SettingsTree tree;
private transient SettingsTree tree;
public JadxSettingsWindow(MainWindow mainWindow, JadxSettings settings) {
this.mainWindow = mainWindow;
@@ -95,7 +94,6 @@ public class JadxSettingsWindow extends JDialog {
this.prevLang = settings.getLangLocale();
initUI();
mainWindow.events().addListener(JadxEvents.RELOAD_SETTINGS_WINDOW, r -> UiUtils.uiRun(this::reloadUI));
setTitle(NLS.str("preferences.title"));
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
@@ -106,16 +104,19 @@ public class JadxSettingsWindow extends JDialog {
if (!mainWindow.getSettings().loadWindowPos(this)) {
setSize(700, 800);
}
mainWindow.events().addListener(JadxEvents.RELOAD_SETTINGS_WINDOW, r -> UiUtils.uiRun(this::reloadUI));
mainWindow.events().addListener(JadxEvents.RELOAD_PROJECT, r -> UiUtils.uiRun(this::reloadUI));
}
private void reloadUI() {
TreePath selectionPath = tree.getSelectionPath();
mainWindow.getSettings().saveWindowPos(this);
int[] selection = tree.getSelectionRows();
getContentPane().removeAll();
initUI();
pack();
mainWindow.getSettings().loadWindowPos(this);
tree.setSelectionPath(selectionPath);
// wait for other events to process
UiUtils.uiRun(() -> {
tree.setSelectionRows(selection);
SwingUtilities.updateComponentTreeUI(this);
});
}
private void initUI() {
@@ -130,7 +131,7 @@ public class JadxSettingsWindow extends JDialog {
groups.add(new ShortcutsSettingsGroup(this, settings));
groups.add(makeSearchResGroup());
groups.add(makeProjectGroup());
groups.add(new PluginsSettings(mainWindow, settings).build());
groups.add(new PluginSettings(mainWindow, settings).build());
groups.add(makeOtherGroup());
tree = new SettingsTree();
@@ -0,0 +1,47 @@
package jadx.gui.settings.ui.plugins;
import jadx.plugins.tools.data.JadxPluginMetadata;
public class AvailablePluginNode extends BasePluginListNode {
private final JadxPluginMetadata metadata;
public AvailablePluginNode(JadxPluginMetadata metadata) {
this.metadata = metadata;
}
@Override
public String getTitle() {
return metadata.getName();
}
@Override
public boolean hasDetails() {
return true;
}
@Override
public String getPluginId() {
return metadata.getPluginId();
}
@Override
public String getDescription() {
return metadata.getDescription();
}
@Override
public String getHomepage() {
return metadata.getHomepage();
}
@Override
public String getLocationId() {
return metadata.getLocationId();
}
@Override
public PluginAction getAction() {
return PluginAction.INSTALL;
}
}
@@ -2,19 +2,33 @@ package jadx.gui.settings.ui.plugins;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.JadxPluginInfo;
abstract class BasePluginListNode {
public @Nullable String getTitle() {
public abstract String getTitle();
public abstract boolean hasDetails();
public String getPluginId() {
return null;
}
public JadxPluginInfo getPluginInfo() {
public String getDescription() {
return null;
}
public String getHomepage() {
return null;
}
public @Nullable String getLocationId() {
return null;
}
public @Nullable String getVersion() {
return null;
}
public PluginAction getAction() {
return PluginAction.NONE;
}
}
@@ -22,29 +22,25 @@ import org.slf4j.LoggerFactory;
import com.formdev.flatlaf.FlatClientProperties;
import ch.qos.logback.classic.Level;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import jadx.gui.logs.LogOptions;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginMetadata;
public class InstallPluginDialog extends JDialog {
private static final Logger LOG = LoggerFactory.getLogger(InstallPluginDialog.class);
private static final long serialVersionUID = 5304314264730563853L;
private final MainWindow mainWindow;
private final PluginSettings pluginsSettings;
private JTextField locationFld;
public InstallPluginDialog(MainWindow mainWindow) {
public InstallPluginDialog(MainWindow mainWindow, PluginSettings pluginsSettings) {
super(mainWindow, NLS.str("preferences.plugins.install"));
this.mainWindow = mainWindow;
this.pluginsSettings = pluginsSettings;
init();
}
@@ -121,20 +117,7 @@ public class InstallPluginDialog extends JDialog {
}
private void install() {
mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.installing"),
() -> {
try {
JadxPluginMetadata metadata = JadxPluginsTools.getInstance().install(locationFld.getText());
LOG.info("Plugin installed: {}", metadata);
} catch (Exception e) {
LOG.error("Install failed", e);
mainWindow.showLogViewer(LogOptions.forLevel(Level.ERROR));
}
},
status -> {
mainWindow.events().send(ReloadSettingsWindow.INSTANCE);
UiUtils.uiRun(mainWindow::reopen);
});
pluginsSettings.install(locationFld.getText());
dispose();
}
}
@@ -0,0 +1,62 @@
package jadx.gui.settings.ui.plugins;
import org.jetbrains.annotations.Nullable;
import jadx.core.plugins.PluginContext;
import jadx.plugins.tools.data.JadxPluginMetadata;
public class InstalledPluginNode extends BasePluginListNode {
private final PluginContext plugin;
private final JadxPluginMetadata metadata;
public InstalledPluginNode(PluginContext plugin, @Nullable JadxPluginMetadata metadata) {
this.plugin = plugin;
this.metadata = metadata;
}
@Override
public @Nullable String getTitle() {
return plugin.getPluginInfo().getName();
}
@Override
public boolean hasDetails() {
return true;
}
@Override
public String getPluginId() {
return plugin.getPluginId();
}
@Override
public String getDescription() {
return plugin.getPluginInfo().getDescription();
}
@Override
public String getHomepage() {
return plugin.getPluginInfo().getHomepage();
}
@Override
public PluginAction getAction() {
if (metadata != null) {
return PluginAction.UNINSTALL;
}
return PluginAction.NONE;
}
@Override
public @Nullable String getVersion() {
if (metadata != null) {
return metadata.getVersion();
}
return null;
}
@Override
public String toString() {
return plugin.getPluginInfo().getName();
}
}
@@ -0,0 +1,7 @@
package jadx.gui.settings.ui.plugins;
public enum PluginAction {
NONE,
INSTALL,
UNINSTALL
}
@@ -1,39 +0,0 @@
package jadx.gui.settings.ui.plugins;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.JadxPluginInfo;
import jadx.core.plugins.PluginContext;
import jadx.plugins.tools.data.JadxPluginMetadata;
public class PluginListNode extends BasePluginListNode {
private final PluginContext plugin;
private final JadxPluginMetadata metadata;
public PluginListNode(PluginContext plugin, @Nullable JadxPluginMetadata metadata) {
this.plugin = plugin;
this.metadata = metadata;
}
@Override
public JadxPluginInfo getPluginInfo() {
return plugin.getPluginInfo();
}
public @Nullable JadxPluginMetadata getMetadata() {
return metadata;
}
@Override
public @Nullable String getVersion() {
if (metadata != null) {
return metadata.getVersion();
}
return null;
}
@Override
public String toString() {
return plugin.getPluginInfo().getName();
}
}
@@ -17,7 +17,9 @@ import javax.swing.JTextField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.events.types.ReloadSettingsWindow;
import ch.qos.logback.classic.Level;
import jadx.api.plugins.events.types.ReloadProject;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.api.plugins.gui.JadxGuiContext;
import jadx.api.plugins.options.JadxPluginOptions;
@@ -26,33 +28,34 @@ import jadx.api.plugins.options.OptionFlag;
import jadx.api.plugins.options.OptionType;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.Utils;
import jadx.gui.logs.LogOptions;
import jadx.gui.plugins.context.GuiPluginContext;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.ui.SettingsGroup;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.plugins.CollectPlugins;
import jadx.gui.utils.ui.DocumentUpdateListener;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginMetadata;
import jadx.plugins.tools.data.JadxPluginUpdate;
public class PluginsSettings {
private static final Logger LOG = LoggerFactory.getLogger(PluginsSettings.class);
public class PluginSettings {
private static final Logger LOG = LoggerFactory.getLogger(PluginSettings.class);
private final MainWindow mainWindow;
private final JadxSettings settings;
public PluginsSettings(MainWindow mainWindow, JadxSettings settings) {
public PluginSettings(MainWindow mainWindow, JadxSettings settings) {
this.mainWindow = mainWindow;
this.settings = settings;
}
public ISettingsGroup build() {
List<PluginContext> list = new CollectPlugins(mainWindow).build();
ISettingsGroup pluginsGroup = new PluginsSettingsGroup(this, list);
for (PluginContext context : list) {
List<PluginContext> installedPlugins = new CollectPlugins(mainWindow).build();
ISettingsGroup pluginsGroup = new PluginSettingsGroup(this, mainWindow, installedPlugins);
for (PluginContext context : installedPlugins) {
ISettingsGroup pluginGroup = addPluginGroup(context);
if (pluginGroup != null) {
pluginsGroup.getSubGroups().add(pluginGroup);
@@ -62,7 +65,25 @@ public class PluginsSettings {
}
public void addPlugin() {
new InstallPluginDialog(mainWindow).setVisible(true);
new InstallPluginDialog(mainWindow, this).setVisible(true);
}
private void requestReload() {
mainWindow.events().send(ReloadProject.INSTANCE);
}
public void install(String locationId) {
mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.installing"),
() -> {
try {
JadxPluginMetadata metadata = JadxPluginsTools.getInstance().install(locationId);
LOG.info("Plugin installed: {}", metadata);
requestReload();
} catch (Exception e) {
LOG.error("Install failed", e);
mainWindow.showLogViewer(LogOptions.forLevel(Level.ERROR));
}
});
}
public void uninstall(String pluginId) {
@@ -70,8 +91,7 @@ public class PluginsSettings {
boolean success = JadxPluginsTools.getInstance().uninstall(pluginId);
if (success) {
LOG.info("Uninstall complete");
mainWindow.events().send(ReloadSettingsWindow.INSTANCE);
UiUtils.uiRun(mainWindow::reopen);
requestReload();
} else {
LOG.warn("Uninstall failed");
}
@@ -83,8 +103,7 @@ public class PluginsSettings {
List<JadxPluginUpdate> updates = JadxPluginsTools.getInstance().updateAll();
if (!updates.isEmpty()) {
LOG.info("Updates: {}\n ", Utils.listToString(updates, "\n "));
mainWindow.events().send(ReloadSettingsWindow.INSTANCE);
UiUtils.uiRun(mainWindow::reopen);
requestReload();
} else {
LOG.info("No updates found");
}
@@ -9,6 +9,8 @@ import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.BorderFactory;
import javax.swing.Box;
@@ -26,27 +28,36 @@ import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import jadx.api.plugins.JadxPluginInfo;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.gui.ISettingsGroup;
import jadx.core.plugins.PluginContext;
import jadx.core.utils.StringUtils;
import jadx.core.utils.Utils;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.plugins.tools.JadxPluginsList;
import jadx.plugins.tools.JadxPluginsTools;
import jadx.plugins.tools.data.JadxPluginMetadata;
class PluginsSettingsGroup implements ISettingsGroup {
private final PluginsSettings pluginsSettings;
class PluginSettingsGroup implements ISettingsGroup {
private static final Logger LOG = LoggerFactory.getLogger(PluginSettingsGroup.class);
private final PluginSettings pluginsSettings;
private final MainWindow mainWindow;
private final String title;
private final List<ISettingsGroup> subGroups = new ArrayList<>();
private final List<PluginContext> pluginsList;
private final List<PluginContext> installedPlugins;
private PluginListNode selectedPlugin;
private JPanel detailsPanel;
public PluginsSettingsGroup(PluginsSettings pluginsSettings, List<PluginContext> pluginsList) {
this.pluginsSettings = pluginsSettings;
public PluginSettingsGroup(PluginSettings pluginSettings, MainWindow mainWindow, List<PluginContext> installedPlugins) {
this.pluginsSettings = pluginSettings;
this.mainWindow = mainWindow;
this.title = NLS.str("preferences.plugins");
this.pluginsList = pluginsList;
this.installedPlugins = installedPlugins;
}
@Override
@@ -83,25 +94,27 @@ class PluginsSettingsGroup implements ISettingsGroup {
installed.forEach(p -> installedMap.put(p.getPluginId(), p));
List<BasePluginListNode> nodes = new ArrayList<>(installed.size() + 3);
for (PluginContext plugin : pluginsList) {
nodes.add(new PluginListNode(plugin, installedMap.get(plugin.getPluginId())));
for (PluginContext plugin : installedPlugins) {
nodes.add(new InstalledPluginNode(plugin, installedMap.get(plugin.getPluginId())));
}
nodes.sort(Comparator.comparing(n -> n.getPluginInfo().getName()));
nodes.sort(Comparator.comparing(BasePluginListNode::getTitle));
DefaultListModel<BasePluginListNode> listModel = new DefaultListModel<>();
listModel.addElement(new TitleNode("Installed"));
nodes.stream().filter(n -> n.getVersion() != null).forEach(listModel::addElement);
listModel.addElement(new TitleNode("Bundled"));
nodes.stream().filter(n -> n.getVersion() == null).forEach(listModel::addElement);
// TODO: load external plugins list
// listModel.addElement(new TitleNode("Available"));
listModel.addElement(new TitleNode("Available"));
JList<BasePluginListNode> pluginsList = new JList<>(listModel);
pluginsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
pluginsList.setCellRenderer(new PluginsListCellRenderer(pluginsList));
pluginsList.addListSelectionListener(ev -> onSelection(pluginsList.getSelectedValue()));
JList<BasePluginListNode> pluginList = new JList<>(listModel);
pluginList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
pluginList.setCellRenderer(new PluginsListCellRenderer());
pluginList.addListSelectionListener(ev -> onSelection(pluginList.getSelectedValue()));
JScrollPane scrollPane = new JScrollPane(pluginsList);
loadAvailablePlugins(listModel, installedPlugins);
JScrollPane scrollPane = new JScrollPane(pluginList);
scrollPane.setMinimumSize(new Dimension(80, 120));
detailsPanel = new JPanel(new BorderLayout(5, 5));
detailsPanel.setBorder(BorderFactory.createCompoundBorder(
@@ -113,7 +126,6 @@ class PluginsSettingsGroup implements ISettingsGroup {
splitPanel.setBorder(BorderFactory.createEmptyBorder(10, 2, 2, 2));
splitPanel.setLeftComponent(scrollPane);
splitPanel.setRightComponent(detailsPanel);
splitPanel.setDividerLocation(0.4);
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BorderLayout(5, 5));
@@ -123,22 +135,45 @@ class PluginsSettingsGroup implements ISettingsGroup {
return mainPanel;
}
private void loadAvailablePlugins(DefaultListModel<BasePluginListNode> listModel, List<PluginContext> installedPlugins) {
List<AvailablePluginNode> list = new ArrayList<>();
mainWindow.getBackgroundExecutor().execute(
NLS.str("preferences.plugins.task.downloading_list"),
() -> {
List<JadxPluginMetadata> availablePlugins;
try {
availablePlugins = JadxPluginsList.getInstance().fetch();
} catch (Exception e) {
LOG.warn("Failed to load available plugins list", e);
return;
}
Set<String> installed = installedPlugins.stream().map(PluginContext::getPluginId).collect(Collectors.toSet());
for (JadxPluginMetadata availablePlugin : availablePlugins) {
if (!installed.contains(availablePlugin.getPluginId())) {
list.add(new AvailablePluginNode(availablePlugin));
}
}
},
status -> listModel.addAll(list));
}
private void onSelection(BasePluginListNode node) {
detailsPanel.removeAll();
JadxPluginInfo pluginInfo = node.getPluginInfo();
if (pluginInfo != null) {
JButton uninstallBtn = new JButton("Uninstall");
if (node.getVersion() != null) {
uninstallBtn.addActionListener(ev -> pluginsSettings.uninstall(pluginInfo.getPluginId()));
} else {
uninstallBtn.setEnabled(false);
}
JLabel nameLbl = new JLabel(pluginInfo.getName());
if (node.hasDetails()) {
JLabel nameLbl = new JLabel(node.getTitle());
Font baseFont = nameLbl.getFont();
nameLbl.setFont(baseFont.deriveFont(Font.BOLD, baseFont.getSize2D() + 2));
String desc;
String homepage = node.getHomepage();
if (StringUtils.notBlank(homepage)) {
desc = node.getDescription() + "\n\nHomepage: " + homepage;
} else {
desc = node.getDescription();
}
JTextPane descArea = new JTextPane();
descArea.setText(pluginInfo.getDescription());
descArea.setText(desc);
descArea.setFont(baseFont.deriveFont(baseFont.getSize2D() + 1));
descArea.setEditable(false);
descArea.setBorder(BorderFactory.createEmptyBorder());
@@ -149,21 +184,41 @@ class PluginsSettingsGroup implements ISettingsGroup {
top.setBorder(BorderFactory.createEmptyBorder(10, 2, 10, 2));
top.add(nameLbl);
top.add(Box.createHorizontalGlue());
top.add(uninstallBtn);
JButton actionBtn = makeActionButton(node);
if (actionBtn != null) {
top.add(actionBtn);
}
detailsPanel.add(top, BorderLayout.PAGE_START);
detailsPanel.add(descArea, BorderLayout.CENTER);
}
detailsPanel.updateUI();
}
private @Nullable JButton makeActionButton(BasePluginListNode node) {
switch (node.getAction()) {
case NONE:
return null;
case INSTALL: {
JButton installBtn = new JButton(NLS.str("preferences.plugins.install_btn"));
installBtn.addActionListener(ev -> pluginsSettings.install(node.getLocationId()));
return installBtn;
}
case UNINSTALL: {
JButton uninstallBtn = new JButton(NLS.str("preferences.plugins.uninstall_btn"));
uninstallBtn.addActionListener(ev -> pluginsSettings.uninstall(node.getPluginId()));
return uninstallBtn;
}
}
return null;
}
private static class PluginsListCellRenderer implements ListCellRenderer<BasePluginListNode> {
private final JPanel panel;
private final JLabel nameLbl;
private final JLabel versionLbl;
private final JLabel titleLbl;
public PluginsListCellRenderer(JList<BasePluginListNode> pluginsList) {
public PluginsListCellRenderer() {
panel = new JPanel();
panel.setOpaque(true);
panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
@@ -176,6 +231,7 @@ class PluginsSettingsGroup implements ISettingsGroup {
versionLbl.setOpaque(true);
panel.add(nameLbl);
panel.add(Box.createHorizontalStrut(20));
panel.add(Box.createHorizontalGlue());
panel.add(versionLbl);
@@ -187,13 +243,12 @@ class PluginsSettingsGroup implements ISettingsGroup {
@Override
public Component getListCellRendererComponent(JList<? extends BasePluginListNode> list,
BasePluginListNode value, int index, boolean isSelected, boolean cellHasFocus) {
String title = value.getTitle();
if (title != null) {
titleLbl.setText(title);
if (!value.hasDetails()) {
titleLbl.setText(value.getTitle());
return titleLbl;
}
nameLbl.setText(value.getPluginInfo().getName());
nameLbl.setToolTipText(value.getPluginInfo().getDescription());
nameLbl.setText(value.getTitle());
nameLbl.setToolTipText(value.getLocationId());
versionLbl.setText(Utils.getOrElse(value.getVersion(), ""));
if (isSelected) {
panel.setBackground(list.getSelectionBackground());
@@ -11,4 +11,9 @@ public class TitleNode extends BasePluginListNode {
public String getTitle() {
return title;
}
@Override
public boolean hasDetails() {
return false;
}
}
@@ -81,6 +81,8 @@ import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.api.ResourceFile;
import jadx.api.plugins.events.IJadxEvents;
import jadx.api.plugins.events.JadxEvents;
import jadx.api.plugins.events.types.ReloadProject;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.core.Jadx;
import jadx.core.export.TemplateFile;
@@ -107,7 +109,7 @@ import jadx.gui.plugins.quark.QuarkDialog;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.ui.JadxSettingsWindow;
import jadx.gui.settings.ui.plugins.InstallPluginDialog;
import jadx.gui.settings.ui.plugins.PluginSettings;
import jadx.gui.treemodel.ApkSignature;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JLoadableNode;
@@ -472,12 +474,14 @@ public class MainWindow extends JFrame {
return loadedFile.resolveSibling(fileName);
}
public synchronized void reopen() {
saveAll();
closeAll();
loadFiles(EMPTY_RUNNABLE);
public void reopen() {
synchronized (ReloadProject.INSTANCE) {
saveAll();
closeAll();
loadFiles(EMPTY_RUNNABLE);
menuBar.reloadShortcuts();
menuBar.reloadShortcuts();
}
}
private void openProject(Path path, Runnable onFinish) {
@@ -576,6 +580,7 @@ public class MainWindow extends JFrame {
initTree();
updateLiveReload(project.isEnableLiveReload());
BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent());
events().addListener(JadxEvents.RELOAD_PROJECT, ev -> UiUtils.uiRun(this::reopen));
List<EditorViewState> openTabs = project.getOpenTabs(this);
backgroundExecutor.execute(NLS.str("progress.load"),
@@ -1519,11 +1524,13 @@ public class MainWindow extends JFrame {
}
public void showLogViewer(LogOptions logOptions) {
if (settings.isDockLogViewer()) {
showDockedLog(logOptions);
} else {
LogViewerDialog.open(this, logOptions);
}
UiUtils.uiRun(() -> {
if (settings.isDockLogViewer()) {
showDockedLog(logOptions);
} else {
LogViewerDialog.open(this, logOptions);
}
});
}
private void showDockedLog(LogOptions logOptions) {
@@ -1555,7 +1562,7 @@ public class MainWindow extends JFrame {
public void resetPluginsMenu() {
pluginsMenu.removeAll();
pluginsMenu.add(new ActionHandler(() -> new InstallPluginDialog(this).setVisible(true))
pluginsMenu.add(new ActionHandler(() -> new PluginSettings(this, settings).addPlugin())
.withNameAndDesc(NLS.str("preferences.plugins.install")));
}
@@ -232,6 +232,7 @@ preferences.res_skip_file=Dateien überspringen (MB)
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.uninstall_btn=Uninstall
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
@@ -240,6 +241,7 @@ preferences.res_skip_file=Dateien überspringen (MB)
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.plugins.task.downloading_list=Downloading plugins list
#preferences.cache=Cache
#preferences.cache.location=Cache location
@@ -232,6 +232,7 @@ preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable)
preferences.plugins.install=Install plugin
preferences.plugins.install_btn=Install
preferences.plugins.uninstall_btn=Uninstall
preferences.plugins.location_id_label=Location id:
preferences.plugins.plugin_jar=Select Plugin jar
preferences.plugins.plugin_jar_label=or
@@ -240,6 +241,7 @@ preferences.plugins.details=Plugin details
preferences.plugins.task.installing=Installing plugin
preferences.plugins.task.uninstalling=Uninstalling plugin
preferences.plugins.task.updating=Updating plugins
preferences.plugins.task.downloading_list=Downloading plugins list
preferences.cache=Cache
preferences.cache.location=Cache location
@@ -232,6 +232,7 @@ preferences.reset_title=Reestablecer preferencias
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.uninstall_btn=Uninstall
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
@@ -240,6 +241,7 @@ preferences.reset_title=Reestablecer preferencias
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.plugins.task.downloading_list=Downloading plugins list
#preferences.cache=Cache
#preferences.cache.location=Cache location
@@ -232,6 +232,7 @@ preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB)
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.uninstall_btn=Uninstall
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
@@ -240,6 +241,7 @@ preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB)
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.plugins.task.downloading_list=Downloading plugins list
#preferences.cache=Cache
#preferences.cache.location=Cache location
@@ -232,6 +232,7 @@ preferences.res_skip_file=Pular arquivos excedidos
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.uninstall_btn=Uninstall
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
@@ -240,6 +241,7 @@ preferences.res_skip_file=Pular arquivos excedidos
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.plugins.task.downloading_list=Downloading plugins list
#preferences.cache=Cache
#preferences.cache.location=Cache location
@@ -232,6 +232,7 @@ preferences.res_skip_file=Пропускать ресурсы больше че
#preferences.plugins.install=Install plugin
#preferences.plugins.install_btn=Install
#preferences.plugins.uninstall_btn=Uninstall
#preferences.plugins.location_id_label=Location id:
#preferences.plugins.plugin_jar=Select Plugin jar
#preferences.plugins.plugin_jar_label=or
@@ -240,6 +241,7 @@ preferences.res_skip_file=Пропускать ресурсы больше че
#preferences.plugins.task.installing=Installing plugin
#preferences.plugins.task.uninstalling=Uninstalling plugin
#preferences.plugins.task.updating=Updating plugins
#preferences.plugins.task.downloading_list=Downloading plugins list
#preferences.cache=Cache
#preferences.cache.location=Cache location
@@ -232,6 +232,7 @@ preferences.res_skip_file=跳过文件大小(MB
preferences.plugins.install=安装插件
preferences.plugins.install_btn=安装
#preferences.plugins.uninstall_btn=Uninstall
preferences.plugins.location_id_label=位置ID
preferences.plugins.plugin_jar=选择插件 jar
preferences.plugins.plugin_jar_label=
@@ -240,6 +241,7 @@ preferences.plugins.details=插件详情
preferences.plugins.task.installing=安装插件中
preferences.plugins.task.uninstalling=卸载插件中
preferences.plugins.task.updating=更新插件中
#preferences.plugins.task.downloading_list=Downloading plugins list
preferences.cache=缓存
preferences.cache.location=缓存位置
@@ -232,6 +232,7 @@ preferences.res_skip_file=略過大於此值的檔案 (MB)
preferences.plugins.install=安裝外掛程式
preferences.plugins.install_btn=安裝
#preferences.plugins.uninstall_btn=Uninstall
preferences.plugins.location_id_label=位置 id
preferences.plugins.plugin_jar=選擇外掛程式 jar
preferences.plugins.plugin_jar_label=
@@ -240,6 +241,7 @@ preferences.plugins.update_all=全部更新
preferences.plugins.task.installing=正在安裝外掛程式
preferences.plugins.task.uninstalling=正在解除安裝外掛程式
preferences.plugins.task.updating=正在更新外掛程式
#preferences.plugins.task.downloading_list=Downloading plugins list
#preferences.cache=Cache
#preferences.cache.location=Cache location
@@ -0,0 +1,86 @@
package jadx.plugins.tools;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.utils.files.FileUtils;
import jadx.plugins.tools.data.JadxPluginMetadata;
import jadx.plugins.tools.resolvers.github.GithubTools;
import jadx.plugins.tools.resolvers.github.LocationInfo;
import jadx.plugins.tools.resolvers.github.data.Asset;
import jadx.plugins.tools.resolvers.github.data.Release;
import jadx.plugins.tools.utils.PluginUtils;
/**
* TODO: implement list caching (on disk) with check for new release
*/
public class JadxPluginsList {
private static final JadxPluginsList INSTANCE = new JadxPluginsList();
private static final Type LIST_TYPE = new TypeToken<List<JadxPluginMetadata>>() {
}.getType();
public static JadxPluginsList getInstance() {
return INSTANCE;
}
private @Nullable List<JadxPluginMetadata> cache;
private JadxPluginsList() {
}
public synchronized List<JadxPluginMetadata> fetch() {
if (cache != null) {
return cache;
}
LocationInfo latest = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list", null);
Release release = GithubTools.fetchRelease(latest);
List<Asset> assets = release.getAssets();
if (assets.isEmpty()) {
throw new RuntimeException("Release don't have assets");
}
Asset listAsset = assets.get(0);
Path tmpListFile = FileUtils.createTempFile("list.zip");
PluginUtils.downloadFile(listAsset.getDownloadUrl(), tmpListFile);
List<JadxPluginMetadata> entries = loadListBundle(tmpListFile);
cache = entries;
return entries;
}
private static List<JadxPluginMetadata> loadListBundle(Path tmpListFile) {
Gson gson = new Gson();
List<JadxPluginMetadata> entries = new ArrayList<>();
ZipSecurity.readZipEntries(tmpListFile.toFile(), (entry, in) -> {
if (entry.getName().endsWith(".json")) {
try (Reader reader = new InputStreamReader(in)) {
entries.addAll(gson.fromJson(reader, LIST_TYPE));
} catch (Exception e) {
throw new RuntimeException("Failed to read plugins list entry: " + entry.getName());
}
}
});
return entries;
}
@TestOnly
public synchronized List<JadxPluginMetadata> fetchFromLocalBundle(Path bundleFile) {
if (cache != null) {
return cache;
}
List<JadxPluginMetadata> entries = loadListBundle(bundleFile);
cache = entries;
return entries;
}
}
@@ -1,10 +1,8 @@
package jadx.plugins.tools;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -12,6 +10,7 @@ import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
@@ -20,8 +19,6 @@ import org.jetbrains.annotations.Nullable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dev.dirs.ProjectDirectories;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginInfo;
import jadx.core.utils.files.FileUtils;
@@ -30,8 +27,11 @@ import jadx.plugins.tools.data.JadxPluginMetadata;
import jadx.plugins.tools.data.JadxPluginUpdate;
import jadx.plugins.tools.resolvers.IJadxPluginResolver;
import jadx.plugins.tools.resolvers.ResolversRegistry;
import jadx.plugins.tools.utils.PluginUtils;
import static jadx.core.utils.files.FileUtils.makeDirs;
import static jadx.plugins.tools.utils.PluginFiles.DROPINS_DIR;
import static jadx.plugins.tools.utils.PluginFiles.INSTALLED_DIR;
import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_JSON;
public class JadxPluginsTools {
private static final JadxPluginsTools INSTANCE = new JadxPluginsTools();
@@ -40,25 +40,19 @@ public class JadxPluginsTools {
return INSTANCE;
}
private final Path pluginsJson;
private final Path dropins;
private final Path installed;
private JadxPluginsTools() {
ProjectDirectories jadxDirs = ProjectDirectories.from("io.github", "skylot", "jadx");
Path plugins = Paths.get(jadxDirs.configDir, "plugins");
makeDirs(plugins);
pluginsJson = plugins.resolve("plugins.json");
dropins = plugins.resolve("dropins");
makeDirs(dropins);
installed = plugins.resolve("installed");
makeDirs(installed);
}
public JadxPluginMetadata install(String locationId) {
JadxPluginMetadata pluginMetadata = resolveMetadata(locationId);
install(pluginMetadata);
return pluginMetadata;
}
public JadxPluginMetadata resolveMetadata(String locationId) {
JadxPluginMetadata pluginMetadata = ResolversRegistry.resolve(locationId)
.orElseThrow(() -> new RuntimeException("Failed to resolve locationId: " + locationId));
install(pluginMetadata);
fillMetadata(pluginMetadata);
return pluginMetadata;
}
@@ -117,14 +111,6 @@ public class JadxPluginsTools {
return true;
}
private void deletePluginJar(JadxPluginMetadata plugin) {
try {
Files.deleteIfExists(installed.resolve(plugin.getJar()));
} catch (IOException e) {
// ignore
}
}
public List<JadxPluginMetadata> getInstalled() {
return loadPluginsJson().getInstalled();
}
@@ -132,9 +118,9 @@ public class JadxPluginsTools {
public List<Path> getAllPluginJars() {
List<Path> list = new ArrayList<>();
for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) {
list.add(installed.resolve(pluginMetadata.getJar()));
list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar()));
}
collectFromDir(list, dropins);
collectFromDir(list, DROPINS_DIR);
return list;
}
@@ -148,28 +134,20 @@ public class JadxPluginsTools {
return null;
}
JadxPluginMetadata update = updateOpt.get();
if (update.getVersion().equals(plugin.getVersion())) {
if (Objects.equals(update.getVersion(), plugin.getVersion())) {
return null;
}
fillMetadata(update);
install(update);
return update;
}
private void install(JadxPluginMetadata metadata) {
Path tmpJar;
if (needDownload(metadata.getJar())) {
tmpJar = FileUtils.createTempFile("plugin.jar");
downloadJar(metadata.getJar(), tmpJar);
} else {
tmpJar = Paths.get(metadata.getJar());
}
fillPluginInfoFromJar(metadata, tmpJar);
public void install(JadxPluginMetadata metadata) {
String version = metadata.getVersion();
String fileName = metadata.getPluginId() + (version != null ? '-' + version : "") + ".jar";
Path pluginJar = installed.resolve(fileName);
copyJar(tmpJar, pluginJar);
metadata.setJar(installed.relativize(pluginJar).toString());
Path pluginJar = INSTALLED_DIR.resolve(fileName);
copyJar(Paths.get(metadata.getJar()), pluginJar);
metadata.setJar(INSTALLED_DIR.relativize(pluginJar).toString());
JadxInstalledPlugins plugins = loadPluginsJson();
// remove previous version jar
@@ -182,28 +160,33 @@ public class JadxPluginsTools {
savePluginsJson(plugins);
}
private void fillPluginInfoFromJar(JadxPluginMetadata metadata, Path jar) {
private void fillMetadata(JadxPluginMetadata metadata) {
Path tmpJar;
if (needDownload(metadata.getJar())) {
tmpJar = FileUtils.createTempFile("plugin.jar");
PluginUtils.downloadFile(metadata.getJar(), tmpJar);
metadata.setJar(tmpJar.toAbsolutePath().toString());
} else {
tmpJar = Paths.get(metadata.getJar());
}
fillMetadataFromJar(metadata, tmpJar);
}
private void fillMetadataFromJar(JadxPluginMetadata metadata, Path jar) {
try (JadxExternalPluginsLoader loader = new JadxExternalPluginsLoader()) {
JadxPlugin jadxPlugin = loader.loadFromJar(jar);
JadxPluginInfo pluginInfo = jadxPlugin.getPluginInfo();
metadata.setPluginId(pluginInfo.getPluginId());
metadata.setName(pluginInfo.getName());
metadata.setDescription(pluginInfo.getDescription());
metadata.setHomepage(pluginInfo.getHomepage());
}
}
private boolean needDownload(String jar) {
private static boolean needDownload(String jar) {
return jar.startsWith("https://") || jar.startsWith("http://");
}
private void downloadJar(String sourceJar, Path destPath) {
try (InputStream in = URI.create(sourceJar).toURL().openStream()) {
Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
throw new RuntimeException("Failed to download jar: " + sourceJar, e);
}
}
private void copyJar(Path sourceJar, Path destJar) {
try {
Files.copy(sourceJar, destJar, StandardCopyOption.REPLACE_EXISTING);
@@ -212,35 +195,45 @@ public class JadxPluginsTools {
}
}
private void deletePluginJar(JadxPluginMetadata plugin) {
try {
Files.deleteIfExists(INSTALLED_DIR.resolve(plugin.getJar()));
} catch (IOException e) {
// ignore
}
}
private static Gson buildGson() {
return new GsonBuilder().setPrettyPrinting().create();
return new GsonBuilder()
.setPrettyPrinting()
.create();
}
private JadxInstalledPlugins loadPluginsJson() {
if (!Files.isRegularFile(pluginsJson)) {
if (!Files.isRegularFile(PLUGINS_JSON)) {
return new JadxInstalledPlugins();
}
try (Reader reader = Files.newBufferedReader(pluginsJson, StandardCharsets.UTF_8)) {
try (Reader reader = Files.newBufferedReader(PLUGINS_JSON, StandardCharsets.UTF_8)) {
return buildGson().fromJson(reader, JadxInstalledPlugins.class);
} catch (Exception e) {
throw new RuntimeException("Failed to read file: " + pluginsJson);
throw new RuntimeException("Failed to read file: " + PLUGINS_JSON);
}
}
private void savePluginsJson(JadxInstalledPlugins data) {
if (data.getInstalled().isEmpty()) {
try {
Files.deleteIfExists(pluginsJson);
Files.deleteIfExists(PLUGINS_JSON);
} catch (Exception e) {
throw new RuntimeException("Failed to remove file: " + pluginsJson, e);
throw new RuntimeException("Failed to remove file: " + PLUGINS_JSON, e);
}
return;
}
data.getInstalled().sort(null);
try (Writer writer = Files.newBufferedWriter(pluginsJson, StandardCharsets.UTF_8)) {
try (Writer writer = Files.newBufferedWriter(PLUGINS_JSON, StandardCharsets.UTF_8)) {
buildGson().toJson(data, writer);
} catch (Exception e) {
throw new RuntimeException("Error saving file: " + pluginsJson, e);
throw new RuntimeException("Error saving file: " + PLUGINS_JSON, e);
}
}
@@ -7,6 +7,7 @@ public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
private String pluginId;
private String name;
private String description;
private String homepage;
private @Nullable String version;
private String locationId;
private String resolverId;
@@ -44,6 +45,14 @@ public class JadxPluginMetadata implements Comparable<JadxPluginMetadata> {
this.description = description;
}
public String getHomepage() {
return homepage;
}
public void setHomepage(String homepage) {
this.homepage = homepage;
}
public String getLocationId() {
return locationId;
}
@@ -6,7 +6,7 @@ import java.util.Optional;
import jadx.plugins.tools.data.JadxPluginMetadata;
import jadx.plugins.tools.resolvers.IJadxPluginResolver;
import static jadx.plugins.tools.utils.PluginsUtils.removePrefix;
import static jadx.plugins.tools.utils.PluginUtils.removePrefix;
public class LocalFileResolver implements IJadxPluginResolver {
@Override
@@ -1,19 +1,8 @@
package jadx.plugins.tools.resolvers.github;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import jadx.core.utils.ListUtils;
import jadx.plugins.tools.data.JadxPluginMetadata;
@@ -21,24 +10,18 @@ import jadx.plugins.tools.resolvers.IJadxPluginResolver;
import jadx.plugins.tools.resolvers.github.data.Asset;
import jadx.plugins.tools.resolvers.github.data.Release;
import static jadx.plugins.tools.utils.PluginsUtils.removePrefix;
import static jadx.plugins.tools.utils.PluginUtils.removePrefix;
public class GithubReleaseResolver implements IJadxPluginResolver {
private static final String GITHUB_API_URL = "https://api.github.com/";
private static final Pattern VERSION_PATTERN = Pattern.compile("v?\\d+\\.\\d+(\\.\\d+)?");
private static final Type RELEASE_TYPE = new TypeToken<Release>() {
}.getType();
private static final Type RELEASE_LIST_TYPE = new TypeToken<List<Release>>() {
}.getType();
@Override
public Optional<JadxPluginMetadata> resolve(String locationId) {
LocationInfo info = parseLocation(locationId);
if (info == null) {
return Optional.empty();
}
Release release = fetchRelease(info);
Release release = GithubTools.fetchRelease(info);
List<Asset> assets = release.getAssets();
String releaseVersion = removePrefix(release.getName(), "v");
Asset asset = searchPluginAsset(assets, info.getArtifactPrefix(), releaseVersion);
@@ -83,7 +66,7 @@ public class GithubReleaseResolver implements IJadxPluginResolver {
if (exactAsset != null) {
return exactAsset;
}
// search without version
// search without version filter
Asset foundAsset = ListUtils.filterOnlyOne(assets, a -> {
String assetFileName = a.getName();
return assetFileName.startsWith(artifactPrefix) && assetFileName.endsWith(".jar");
@@ -102,42 +85,6 @@ public class GithubReleaseResolver implements IJadxPluginResolver {
return baseLocation + ':' + info.getArtifactPrefix();
}
private static Release fetchRelease(LocationInfo info) {
String projectUrl = GITHUB_API_URL + "repos/" + info.getOwner() + "/" + info.getProject();
String version = info.getVersion();
if (version == null) {
// get latest version
return get(projectUrl + "/releases/latest", RELEASE_TYPE);
}
// search version among all releases (by name)
List<Release> releases = get(projectUrl + "/releases", RELEASE_LIST_TYPE);
return releases.stream()
.filter(r -> r.getName().equals(version))
.findFirst()
.orElseThrow(() -> new RuntimeException("Release with version: " + version + " not found."
+ " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", "))));
}
private static <T> T get(String url, Type type) {
HttpURLConnection con;
try {
con = (HttpURLConnection) URI.create(url).toURL().openConnection();
con.setRequestMethod("GET");
int code = con.getResponseCode();
if (code != 200) {
// TODO: support redirects?
throw new RuntimeException("Request failed, response: " + code + ", url: " + url);
}
} catch (IOException e) {
throw new RuntimeException("Request failed, url: " + url, e);
}
try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) {
return new Gson().fromJson(reader, type);
} catch (Exception e) {
throw new RuntimeException("Failed to parse response, url: " + url, e);
}
}
@Override
public String id() {
return "github-release";
@@ -0,0 +1,61 @@
package jadx.plugins.tools.resolvers.github;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import jadx.plugins.tools.resolvers.github.data.Release;
public class GithubTools {
private static final String GITHUB_API_URL = "https://api.github.com/";
private static final Type RELEASE_TYPE = new TypeToken<Release>() {
}.getType();
private static final Type RELEASE_LIST_TYPE = new TypeToken<List<Release>>() {
}.getType();
public static Release fetchRelease(LocationInfo info) {
String projectUrl = GITHUB_API_URL + "repos/" + info.getOwner() + "/" + info.getProject();
String version = info.getVersion();
if (version == null) {
// get latest version
return get(projectUrl + "/releases/latest", RELEASE_TYPE);
}
// search version among all releases (by name)
List<Release> releases = get(projectUrl + "/releases", RELEASE_LIST_TYPE);
return releases.stream()
.filter(r -> r.getName().equals(version))
.findFirst()
.orElseThrow(() -> new RuntimeException("Release with version: " + version + " not found."
+ " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", "))));
}
private static <T> T get(String url, Type type) {
HttpURLConnection con;
try {
con = (HttpURLConnection) URI.create(url).toURL().openConnection();
con.setRequestMethod("GET");
int code = con.getResponseCode();
if (code != 200) {
// TODO: support redirects?
throw new RuntimeException("Request failed, response: " + code + ", url: " + url);
}
} catch (IOException e) {
throw new RuntimeException("Request failed, url: " + url, e);
}
try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) {
return new Gson().fromJson(reader, type);
} catch (Exception e) {
throw new RuntimeException("Failed to parse response, url: " + url, e);
}
}
}
@@ -2,7 +2,7 @@ package jadx.plugins.tools.resolvers.github;
import org.jetbrains.annotations.Nullable;
class LocationInfo {
public class LocationInfo {
private final String owner;
private final String project;
private final String artifactPrefix;
@@ -0,0 +1,24 @@
package jadx.plugins.tools.utils;
import java.nio.file.Path;
import java.nio.file.Paths;
import dev.dirs.ProjectDirectories;
import static jadx.core.utils.files.FileUtils.makeDirs;
public class PluginFiles {
private static final ProjectDirectories DIRS = ProjectDirectories.from("io.github", "skylot", "jadx");
public static final Path PLUGINS_DIR = Paths.get(DIRS.configDir, "plugins");
public static final Path PLUGINS_JSON = PLUGINS_DIR.resolve("plugins.json");
public static final Path INSTALLED_DIR = PLUGINS_DIR.resolve("installed");
public static final Path DROPINS_DIR = PLUGINS_DIR.resolve("dropins");
public static final Path CACHE_DIR = Paths.get(DIRS.cacheDir);
static {
makeDirs(INSTALLED_DIR);
makeDirs(DROPINS_DIR);
}
}
@@ -0,0 +1,25 @@
package jadx.plugins.tools.utils;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class PluginUtils {
public static String removePrefix(String str, String prefix) {
if (str.startsWith(prefix)) {
return str.substring(prefix.length());
}
return str;
}
public static void downloadFile(String fileUrl, Path destPath) {
try (InputStream in = URI.create(fileUrl).toURL().openStream()) {
Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
throw new RuntimeException("Failed to download file: " + fileUrl, e);
}
}
}
@@ -1,11 +0,0 @@
package jadx.plugins.tools.utils;
public class PluginsUtils {
public static String removePrefix(String str, String prefix) {
if (str.startsWith(prefix)) {
return str.substring(prefix.length());
}
return str;
}
}
@@ -18,14 +18,9 @@ import jadx.plugins.input.java.utils.JavaClassParseException;
public class JavaInputPlugin implements JadxPlugin {
public static final JadxPluginInfo PLUGIN_INFO = new JadxPluginInfo(
"java-input",
"JavaInput",
"Load .class and .jar files");
@Override
public JadxPluginInfo getPluginInfo() {
return PLUGIN_INFO;
return new JadxPluginInfo("java-input", "Java Input", "Load .class and .jar files");
}
@Override
@@ -15,10 +15,7 @@ public class RaungInputPlugin implements JadxPlugin, JadxCodeInput {
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo(
"raung-input",
"RaungInput",
"Load .raung files");
return new JadxPluginInfo("raung-input", "Raung Input", "Load .raung files");
}
@Override
@@ -17,7 +17,7 @@ public class SmaliInputPlugin implements JadxPlugin, JadxCodeInput {
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo("smali-input", "SmaliInput", "Load .smali files");
return new JadxPluginInfo("smali-input", "Smali Input", "Load .smali files");
}
@Override