Merge remote-tracking branch

'origin/GP-6641-dragonmacher-extensions-preview' (#8984)
This commit is contained in:
Ryan Kurtz
2026-04-13 12:12:27 -04:00
33 changed files with 769 additions and 357 deletions
@@ -14,8 +14,8 @@
into a Ghidra distribution. This allows users to create and share new plugins and scripts.
Ghidra ships with some pre-built extensions that not installed by default.
</P>
<P>Ghidra Extensions can be installed and uninstalled at runtime, with the changes taking effect
when Ghidra is restarted. The extension installation dialog can
<P>Ghidra Extensions can be installed and uninstalled at runtime, <U>with the changes taking effect
when Ghidra is restarted</U>. The extension installation dialog can
be opened by selecting the <B>Install Extensions</B> option on the project <B>File</B> menu.</P>
<BLOCKQUOTE>
@@ -32,11 +32,61 @@
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>Installed extensions provide new functionality to Ghidra, such as Plugins, Analyzers and
other Extension Points. Non-plugin Extension Points will be automatically discovered and
loaded at startup. This is <B>not</B> the case for Plugins. Plugins must be manually
enabled inside of the tool that is being used. For example, when running the Code Browser,
use the <A href="help/topics/Tool/Configure_Tool.htm">
<B>File</B><IMG src="help/shared/arrow.gif" border="0"><B>Congfigure</B></A> menu to
enable any plugins added by newly installed extensions.
</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0">The easiest way to find the plugins
for a given extension is to use the table view of all known plugins. To see all plugins
for an extension:</P>
<OL>
<LI>
Configure plugins via <B>File</B><IMG src="help/shared/arrow.gif" border="0">
<B>Congfigure</B>
</LI>
<LI>
Click the <img src="images/plugin.png" /> icon to show all plugins
</LI>
<LI>
If not already visible, add the <B>Module</B> table column via the right-click
menu of the table header. (Extensions are modules.)
</LI>
<LI>
Sort on the <B>Module</B> table column
</LI>
<LI>
Optionally, you can type the name of the extension into the filter to hide other modules
</LI>
</OL>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2>Dialog Components</H2>
<H3>Extensions Table</H3>
<BLOCKQUOTE>
<P>To install an extension, select the extension's checkbox. To unininstall, deselect the
checkbox.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/note.png" alt="" border="0">If the checkbox is not editable,
that means that means the extension is installed and canont be uninistalled. This can
happen in development mode with extensions that live in source control.</P>
</BLOCKQUOTE>
<P>The list of extensions is populated when the dialog is launched. To build the list, Ghidra
looks in several locations:</P>
@@ -371,8 +371,8 @@
<P class="relatedtopic">Related Topics</P>
<UL>
<LI><A href="help/topics/Tool/ToolOptions_Dialog.htm#KeyBindings_Option">Key
Bindings</A></LI>
<LI><A href="../BundleManager/BundleManager.htm">Ghidra Bundles</A>
<LI><A href="help/topics/Tool/ToolOptions_Dialog.htm#KeyBindings_Option">Key Bindings</A></LI>
</UL>
<P>&nbsp;</P>
@@ -27,7 +27,8 @@
<UL>
<LI>
<B>Version</B> - Ghidra, Operating System, and Java version information. Clicking the
<B><I>Copy</I></B> button will copy this information to the clipboard for easy transfer into a bug report.
<B><I>Copy</I></B> button will copy this information to the clipboard for easy transfer into
a bug report.
</LI>
<LI>
<B>Memory</B> - JVM memory usage. Clicking the <B><I>Collect Garbage</I></B> button will
@@ -47,7 +48,11 @@
<B>Modules</B> - A list of discovered Ghidra Modules.
</LI>
<LI>
<B>Extension Points</B> - A list of discovered Ghidra Extension Points.
<B>Extension Points</B> - A list of discovered Ghidra Extension Points.<BR><BR>
Note: Extension Points differ from Extensions. Extension Points are classes that Ghidra
will find at startup. Extensions are additional Ghidra Modules that can be installed
by the user. Further, Extensions may contain Extension Points.
<BR><BR>
</LI>
<LI>
<B>Classpath</B> - The ordered classpath for the active Ghidra application.
@@ -24,6 +24,7 @@ import java.util.*;
import org.junit.AfterClass;
import docking.test.AbstractDockingTest;
import generic.jar.ResourceFile;
import ghidra.GhidraTestApplicationLayout;
import ghidra.app.events.ProgramLocationPluginEvent;
import ghidra.app.events.ProgramSelectionPluginEvent;
@@ -659,9 +660,16 @@ public abstract class AbstractGhidraHeadlessIntegrationTest extends AbstractDock
Set<ClassFileInfo> serviceSet = extensionPointSuffixToInfoMap.get(suffix);
assertNotNull(serviceSet);
serviceSet.clear();
ClassFileInfo info = new ClassFileInfo("", replacement.getClass().getName(), suffix);
Class<? extends Object> clazz = replacement.getClass();
ResourceFile module = Application.getModuleContainingClass(clazz);
String modulePath = "";
if (module != null) {
modulePath = module.getAbsolutePath();
}
String name = clazz.getName();
ClassFileInfo info = new ClassFileInfo("", name, suffix, modulePath);
serviceSet.add(info);
loadedCache.put(info, replacement.getClass());
loadedCache.put(info, clazz);
}
T instance = tool.getService(service);
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -263,7 +263,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
assertNotNull(analyzerSet);
analyzerSet.removeIf(c -> c.name().contains("TestStubAnalyzer"));
ClassFileInfo info = new ClassFileInfo("", analyzer.getName(), "Analyzer");
ClassFileInfo info = new ClassFileInfo("", analyzer.getName(), "Analyzer", "");
analyzerSet.add(info);
loadedCache.put(info, analyzer);
}
@@ -31,13 +31,14 @@ import ghidra.framework.Application;
import ghidra.framework.project.tool.testplugins.TestExtensionHello2Plugin;
import ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.xml.GenericXMLOutputter;
import ghidra.util.xml.XmlUtilities;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
public class ExtensionManagerTest extends AbstractGhidraHeadedIntegrationTest {
public class ToolExtensionStatusManagerTest extends AbstractGhidraHeadedIntegrationTest {
private ApplicationLayout appLayout;
private FakeToolExtensionsEnabledState extensionsState;
@@ -130,6 +131,7 @@ public class ExtensionManagerTest extends AbstractGhidraHeadedIntegrationTest {
// verify user is prompted to add new plugins
extensionManager.checkForNewExtensions();
waitForSwing();
waitForSwing();
assertTrue(extensionsState.didPrompt());
}
@@ -274,35 +276,47 @@ public class ExtensionManagerTest extends AbstractGhidraHeadedIntegrationTest {
private class FakeToolExtensionsEnabledState implements ExtensionsEnabledState {
private Map<String, Set<Class<?>>> extensions = new HashMap<>();
private Set<Class<?>> installedPlugins = new HashSet<>();
private Map<String, Set<ClassFileInfo>> extensions = new HashMap<>();
private Set<ClassFileInfo> installedPlugins = new HashSet<>();
private boolean didPrompt = false;
@Override
public Map<String, Set<Class<?>>> getAllKnownExtensions() {
public Map<String, Set<ClassFileInfo>> getAllKnownExtensions() {
return extensions;
}
@Override
public void removeInstalledPlugins(Set<Class<?>> plugins) {
public void removeInstalledPlugins(Set<ClassFileInfo> plugins) {
plugins.removeAll(installedPlugins);
}
@Override
public void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
public void propmtToConfigureNewPlugins(Set<ClassFileInfo> plugins) {
didPrompt = true;
}
void addExtension(String name, Class<?>... classes) {
void addExtension(String name, ClassFileInfo... classes) {
extensions.put(name, Set.of(classes));
}
void addExtension(String name, Class<?>... classes) {
for (Class<?> c : classes) {
String className = c.getName();
ClassFileInfo info = new ClassFileInfo("/fake/path", className, "Plugin", "");
addExtension(name, info);
}
}
boolean didPrompt() {
return didPrompt;
}
void setInstalled(Class<TestExtensionHelloPlugin> c) {
installedPlugins.add(c);
String name = c.getName();
ClassFileInfo info = new ClassFileInfo("/fake/path", name, "Plugin", "");
installedPlugins.add(info);
}
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -34,8 +34,10 @@ import docking.dnd.GClipboard;
import docking.dnd.StringTransferable;
import docking.widgets.label.GDLabel;
import docking.widgets.label.GLabel;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors;
import generic.theme.GThemeDefaults.Colors.Messages;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.util.ColorUtils;
import ghidra.util.WebColors;
import ghidra.util.layout.HorizontalLayout;
@@ -351,12 +353,20 @@ public class SettableColorSwatchChooserPanel extends AbstractColorChooserPanel {
String text = colorNameField.getText();
String colorText = text.replaceAll("\s", "");
Color color = WebColors.getColor(colorText);
if (color == null) {
color = WebColors.getColor('#' + colorText);
}
if (color == null) {
GColor gColor = Palette.getColor(colorText);
if (!gColor.isUnresolved()) {
color = gColor;
}
}
if (color != null) {
getColorSelectionModel().setSelectedColor(color);
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,31 +18,40 @@ package ghidra.util.classfinder;
import java.io.File;
import java.util.List;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
class ClassDir {
private String dirPath;
private File dir;
private String modulePath = "";
private ClassPackage classPackage;
ClassDir(String dirPath, TaskMonitor monitor) throws CancelledException {
this.dirPath = dirPath;
this.dir = new File(dirPath);
classPackage = new ClassPackage(dir, "", monitor);
ClassDir(File dir, TaskMonitor monitor) throws CancelledException {
this.dir = dir;
ResourceFile module = Application.getModuleContainingResourceFile(new ResourceFile(dir));
if (module != null) {
modulePath = module.getAbsolutePath();
}
classPackage = new ClassPackage(this, "", monitor);
}
void getClasses(List<ClassFileInfo> list, TaskMonitor monitor) throws CancelledException {
classPackage.getClasses(list, monitor);
}
String getDirPath() {
return dirPath;
File getDir() {
return dir;
}
String getModulePath() {
return modulePath;
}
@Override
public String toString() {
return dirPath;
return dir.toString();
}
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -21,5 +21,18 @@ package ghidra.util.classfinder;
* @param path The path to the class file (or jar containing the class)
* @param name The name of the class (including package)
* @param suffix The class suffix (i.e., extension point type name)
* @param module The module path for this class
*/
public record ClassFileInfo(String path, String name, String suffix) {}
public record ClassFileInfo(String path, String name, String suffix, String module) {
/**
* {@return the simple class name (no package name) for the class represented by this info}
*/
public String simpleName() {
int index = name.lastIndexOf('.');
if (index < 0) {
return name; // no package
}
return name.substring(index + 1);
}
}
@@ -52,12 +52,15 @@ class ClassJar implements ClassLocation {
private static final Set<String> USER_PLUGIN_PATHS = loadUserPluginPaths();
private Set<ClassFileInfo> classes = new HashSet<>();
private String path;
ClassJar(String path, TaskMonitor monitor) throws CancelledException {
this.path = path;
loadUserPluginPaths();
private File file;
private String modulePath = "";
ClassJar(File file, TaskMonitor monitor) throws CancelledException {
this.file = file;
ResourceFile module = Application.getModuleContainingResourceFile(new ResourceFile(file));
if (module != null) {
modulePath = module.getAbsolutePath();
}
scanJar(monitor);
}
@@ -68,8 +71,6 @@ class ClassJar implements ClassLocation {
private void scanJar(TaskMonitor monitor) throws CancelledException {
File file = new File(path);
try (JarFile jarFile = new JarFile(file)) {
String pathName = jarFile.getName();
@@ -84,7 +85,7 @@ class ClassJar implements ClassLocation {
}
}
catch (IOException e) {
Msg.error(this, "Error reading jarFile: " + path, e);
Msg.error(this, "Error reading jarFile: " + file, e);
}
}
@@ -176,13 +177,14 @@ class ClassJar implements ClassLocation {
String epName = ClassSearcher.getExtensionPointSuffix(name);
if (epName != null) {
classes.add(new ClassFileInfo(path, name, epName));
String path = file.getAbsolutePath();
classes.add(new ClassFileInfo(path, name, epName, modulePath));
}
}
@Override
public String toString() {
return path;
return file.toString();
}
private static String getPatchDirPath() {
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -29,28 +29,33 @@ class ClassPackage implements ClassLocation {
pathname -> pathname.getName().endsWith(CLASS_EXT);
private Set<ClassPackage> children = new HashSet<>();
private File rootDir;
private ClassDir classDir;
private File packageDir;
private String packageName;
private Set<ClassFileInfo> classes = new HashSet<>();
ClassPackage(File rootDir, String packageName, TaskMonitor monitor) throws CancelledException {
ClassPackage(ClassDir classDir, String packageName, TaskMonitor monitor)
throws CancelledException {
monitor.checkCancelled();
this.rootDir = rootDir;
this.classDir = classDir;
File rootDir = classDir.getDir();
this.packageName = packageName;
this.packageDir = getPackageDir(rootDir, packageName);
scanClasses();
this.packageDir = new File(rootDir, packageName.replace('.', File.separatorChar));
scanClasses(rootDir);
scanSubPackages(monitor);
}
private void scanClasses() {
private void scanClasses(File rootDir) {
String path = rootDir.getAbsolutePath();
Set<String> allClassNames = getAllClassNames();
for (String className : allClassNames) {
String epName = ClassSearcher.getExtensionPointSuffix(className);
if (epName != null) {
classes.add(new ClassFileInfo(path, className, epName));
String module = classDir.getModulePath();
classes.add(new ClassFileInfo(path, className, epName, module));
}
}
}
@@ -80,24 +85,18 @@ class ClassPackage implements ClassLocation {
}
monitor.setMessage("Scanning package: " + pkg);
children.add(new ClassPackage(rootDir, pkg, monitor));
children.add(new ClassPackage(classDir, pkg, monitor));
}
}
private File getPackageDir(File lRootDir, String lPackageName) {
return new File(lRootDir, lPackageName.replace('.', File.separatorChar));
}
@Override
public void getClasses(List<ClassFileInfo> list, TaskMonitor monitor)
throws CancelledException {
list.addAll(classes);
Iterator<ClassPackage> it = children.iterator();
while (it.hasNext()) {
for (ClassPackage subPkg : children) {
monitor.checkCancelled();
ClassPackage subPkg = it.next();
subPkg.getClasses(list, monitor);
}
}
@@ -168,8 +168,10 @@ public class ClassSearcher {
"Cannot call the getClasses() while the ClassSearcher is searching!");
}
String suffix = getExtensionPointSuffix(ancestorClass.getName());
String className = ancestorClass.getName();
String suffix = getExtensionPointSuffix(className);
if (suffix == null) {
Msg.error(ClassSearcher.class, "Class is not a known extension point: " + className);
return List.of();
}
@@ -414,8 +416,11 @@ public class ClassSearcher {
for (String searchPath : gatherSearchPaths()) {
String lcSearchPath = searchPath.toLowerCase();
File searchFile = new File(searchPath);
if ((lcSearchPath.endsWith(".jar") || lcSearchPath.endsWith(".zip")) &&
searchFile.exists()) {
if (!searchFile.exists()) {
continue;
}
if (lcSearchPath.endsWith(".jar") || lcSearchPath.endsWith(".zip")) {
if (ClassJar.ignoreJar(searchPath)) {
log.trace("Ignoring jar file: {}", searchPath);
@@ -423,11 +428,11 @@ public class ClassSearcher {
}
log.trace("Searching jar file: {}", searchPath);
classJars.add(new ClassJar(searchPath, monitor));
classJars.add(new ClassJar(searchFile, monitor));
}
else if (searchFile.isDirectory()) {
log.trace("Searching classpath directory: {}", searchPath);
classDirs.add(new ClassDir(searchPath, monitor));
classDirs.add(new ClassDir(searchFile, monitor));
}
}
@@ -533,8 +538,8 @@ public class ClassSearcher {
for (String className : classNames) {
String epName = getExtensionPointSuffix(className);
if (epName != null) {
extensionClasses
.add(new ClassFileInfo(appRoot.getAbsolutePath(), className, epName));
String path = appRoot.getAbsolutePath();
extensionClasses.add(new ClassFileInfo(path, className, epName, ""));
}
}
return extensionClasses.stream()
@@ -120,6 +120,9 @@ public class ExtensionUtils {
return success;
}
/**
* {@return all installed extensions that are not marked for uninstall}
*/
public static Set<ExtensionDetails> getActiveInstalledExtensions() {
return getAllInstalledExtensions().getActiveExtensions();
}
@@ -291,8 +291,8 @@ public class ApplicationThemeManager extends ThemeManager {
update(() -> {
updateChangedValuesMap(currentValue, newValue);
currentValues.addColor(newValue);
notifyThemeChanged(new ColorChangedThemeEvent(currentValues, newValue));
lookAndFeelManager.colorsChanged();
notifyThemeChanged(new ColorChangedThemeEvent(currentValues, newValue));
});
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -135,7 +135,7 @@ public class GColor extends Color {
* @return true if this GColor could not find a value for its color id in the current theme
*/
public boolean isUnresolved() {
return delegate == ColorValue.LAST_RESORT_DEFAULT;
return delegate == ColorValue.LAST_RESORT_DEFAULT || delegate == ThemeManager.DEFAULT_COLOR;
}
@Override
@@ -135,6 +135,8 @@ public class GThemeDefaults {
*/
public static class Palette {
private static final String PALETTE_PREFIX = "color.palette.";
/** Transparent color */
public static final Color NO_COLOR = getColor("nocolor");
@@ -174,7 +176,10 @@ public class GThemeDefaults {
* @return the GColor
*/
public static GColor getColor(String name) {
return new GColor("color.palette." + name);
if (name.startsWith(PALETTE_PREFIX)) {
return new GColor(name);
}
return new GColor(PALETTE_PREFIX + name);
}
}
}
@@ -7,12 +7,14 @@ color.fg.extensionpanel.details.name = color.palette.limegreen
color.fg.extensionpanel.path = color.palette.blue
color.fg.extensionpanel.details.title = color.palette.maroon
color.fg.extensionpanel.details.version = color.palette.blue
color.fg.extensionpanel.details.classes.header = color.palette.maroon
color.fg.extensionpanel.details.classes.type = color.palette.darkkhaki
color.fg.options.keybindings.table.unregistered = color.palette.lightgray
color.fg.options.keybindings.table.unregistered.selected.unfocused = color.palette.gray
color.fg.pluginpanel.name = color.fg
color.fg.pluginpanel.description = color.palette.gray
color.fg.plugin.package.panel.name = color.fg
color.fg.plugin.package.panel.description = color.palette.gray
color.bg.panel.details = color.bg
color.fg.pluginpanel.details.title = color.palette.maroon
@@ -90,9 +92,10 @@ font.task.viewer = sansserif-bold-36
font.task.progress.label.message = sansserif-plain-12
font.user.agreement = sansserif-italic-22
font.panel.details = font.standard
font.panel.details.monospaced = font.monospaced[bold]
font.pluginpanel.name = sansserif-plain-18
font.plugin.details.panel.default = font.standard
font.plugin.details.panel.monospaced = font.monospaced[bold]
font.plugin.package.panel.name = sansserif-plain-18
@@ -55,7 +55,7 @@ import ghidra.framework.plugintool.dialog.ManagePluginsDialog;
import ghidra.framework.plugintool.mgr.*;
import ghidra.framework.plugintool.util.*;
import ghidra.framework.project.ProjectDataService;
import ghidra.framework.project.extensions.ExtensionTableProvider;
import ghidra.framework.project.extensions.ExtensionTableDialog;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
@@ -339,7 +339,7 @@ public abstract class PluginTool extends AbstractDockingTool {
* Displays the extensions installation dialog.
*/
public void showExtensions() {
showDialog(new ExtensionTableProvider(this));
showDialog(new ExtensionTableDialog(this));
}
/**
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,10 +17,10 @@ package ghidra.framework.plugintool.dialog;
import static ghidra.util.HTMLUtilities.*;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.*;
import javax.swing.*;
import javax.swing.text.View;
import docking.widgets.label.GDHtmlLabel;
import generic.theme.*;
@@ -30,12 +30,13 @@ import generic.theme.*;
*/
public abstract class AbstractDetailsPanel extends JPanel {
protected static final String FONT_DEFAULT = "font.panel.details";
protected static final String FONT_MONOSPACED = "font.panel.details.monospaced";
protected static final String FONT_DEFAULT = "font.plugin.details.panel.default";
protected static final String FONT_MONOSPACED = "font.plugin.details.panel.monospaced";
// based on the default font size
private static final int PREFERRED_WIDTH = 900;
private static final int LEFT_COLUMN_WIDTH = 150;
private static final int MIN_WIDTH = 700;
protected static final int LEFT_COLUMN_WIDTH = 150;
protected static final int RIGHT_MARGIN = 30;
// Font attributes for the title of each row.
protected static GAttributes titleAttrs;
@@ -44,10 +45,7 @@ public abstract class AbstractDetailsPanel extends JPanel {
protected JScrollPane sp;
private ThemeListener themeListener = e -> {
if (e.isFontChanged(FONT_DEFAULT) || e.isFontChanged(FONT_MONOSPACED)) {
updateFieldAttributes();
}
updateFieldAttributes();
};
protected AbstractDetailsPanel() {
@@ -81,15 +79,39 @@ public abstract class AbstractDetailsPanel extends JPanel {
*/
protected void createMainPanel() {
setLayout(new BorderLayout());
textLabel = new GDHtmlLabel() {
@Override
public Dimension getPreferredSize() {
// overridden to force word-wrapping by limiting the preferred size of the label
Dimension mySize = super.getPreferredSize();
int rightColumnWidth = AbstractDetailsPanel.this.getWidth() - LEFT_COLUMN_WIDTH;
mySize.width = Math.max(MIN_WIDTH, rightColumnWidth);
return mySize;
// Overridden to force word-wrapping by limiting the preferred size of the label.
// Specifically, long descriptions will get word-wrapped by the html document when
// the text is longer than the preferred width.
Dimension size = super.getPreferredSize();
int availableRightWidth = AbstractDetailsPanel.this.getWidth() - LEFT_COLUMN_WIDTH;
size.width = Math.max(MIN_WIDTH, availableRightWidth);
View v = (View) getClientProperty("html");
if (v == null) {
return size;
}
// We may have html wider than the chosen width that cannot wrap due to not having
// any whitespace (e.g., a file path). In that case, the display will run past the
// right edge of the screen without showing a horizontal scrollbar. We can force
// the scroll bar to appear by making the chosen width be the html minimum width
// (which is the width needed to render an unbreakable line of text).
Insets i = getInsets();
int availableWidth = size.width - (i.left + i.right);
int htmlw = (int) v.getMinimumSpan(View.X_AXIS);
boolean isClipped = htmlw > availableWidth;
if (isClipped) {
// the minimum html is wider than the current preferred width
size.width = htmlw;
}
return size;
}
};
@@ -98,7 +120,7 @@ public abstract class AbstractDetailsPanel extends JPanel {
textLabel.setBackground(new GColor("color.bg.panel.details"));
sp = new JScrollPane(textLabel);
sp.getVerticalScrollBar().setUnitIncrement(10);
sp.setPreferredSize(new Dimension(MIN_WIDTH, 200));
sp.setPreferredSize(new Dimension(PREFERRED_WIDTH, 300));
add(sp, BorderLayout.CENTER);
}
@@ -110,9 +132,13 @@ public abstract class AbstractDetailsPanel extends JPanel {
* @param rowName the name of the row to add
*/
protected void insertRowTitle(StringBuilder buffer, String rowName) {
insertRowTitle(buffer, rowName, titleAttrs);
}
protected void insertRowTitle(StringBuilder buffer, String rowName, GAttributes attributes) {
buffer.append("<TR>");
buffer.append("<TD VALIGN=\"TOP\">");
insertHTMLLine(buffer, rowName + ":", titleAttrs);
buffer.append("<TD VALIGN=\"TOP\" NOWRAP>");
insertHTMLLine(buffer, rowName + ":", attributes);
buffer.append("</TD>");
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -111,6 +111,15 @@ class PluginDetailsPanel extends AbstractDetailsPanel {
insertRowTitle(buffer, "Category");
insertRowValue(buffer, descriptor.getCategory(), categoriesAttrs);
if (descriptor.isInExtension()) {
insertRowTitle(buffer, "Extension");
}
else {
insertRowTitle(buffer, "Module");
}
String module = descriptor.getModuleName();
insertRowValue(buffer, module, categoriesAttrs);
insertRowTitle(buffer, "Plugin Class");
insertRowValue(buffer, descriptor.getPluginClass().getName(), classAttrs);
@@ -175,8 +175,8 @@ public class PluginManagerComponent extends JPanel implements Scrollable {
labelPanel.setBackground(BG);
GLabel nameLabel = new GLabel(pluginPackage.getName());
Gui.registerFont(nameLabel, "font.pluginpanel.name");
nameLabel.setForeground(new GColor("color.fg.pluginpanel.name"));
Gui.registerFont(nameLabel, "font.plugin.package.panel.name");
nameLabel.setForeground(new GColor("color.fg.plugin.package.panel.name"));
labelPanel.add(nameLabel);
GHyperlinkComponent configureHyperlink = createConfigureHyperlink();
@@ -206,7 +206,7 @@ public class PluginManagerComponent extends JPanel implements Scrollable {
String htmlDescription = enchanceDescription(pluginPackage.getDescription());
JLabel descriptionlabel = new GHtmlLabel(htmlDescription);
descriptionlabel.setForeground(new GColor("color.fg.pluginpanel.description"));
descriptionlabel.setForeground(new GColor("color.fg.plugin.package.panel.description"));
descriptionlabel.setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0));
descriptionlabel.setVerticalAlignment(SwingConstants.TOP);
descriptionlabel.setToolTipText(
@@ -25,6 +25,8 @@ import ghidra.app.plugin.PluginCategoryNames;
import ghidra.framework.Application;
import ghidra.framework.plugintool.*;
import ghidra.util.Msg;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
/**
* Class to hold meta information about a plugin, derived from meta-data attached to
@@ -124,11 +126,11 @@ public class PluginDescription implements Comparable<PluginDescription> {
if (path.startsWith(fileProtoPrefix)) {
path = path.substring(fileProtoPrefix.length() + 1);
}
return path;
return '/' + path;
}
String classpath = pluginClass.getName();
path = path.substring(0, path.length() - classpath.length() - DOTCLASS_EXT.length() - 1);
return path;
return '/' + path;
}
/**
@@ -149,7 +151,7 @@ public class PluginDescription implements Comparable<PluginDescription> {
}
/**
* Return the name of the module that contains the plugin.
* Returns the name of the module that contains the plugin.
* @return the module name
*/
public String getModuleName() {
@@ -161,6 +163,17 @@ public class PluginDescription implements Comparable<PluginDescription> {
return moduleName;
}
/**
* {@return true if this plugin is provided by an extension}
*/
public boolean isInExtension() {
String myPath = getSourceLocation();
ResourceFile myLocation = new ResourceFile(myPath);
ApplicationLayout layout = Application.getApplicationLayout();
List<ResourceFile> extDirs = layout.getExtensionInstallationDirs();
return FileUtilities.isPathContainedWithin(extDirs, myLocation);
}
/**
* Return the class of the plugin.
* @return plugin class object
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,10 +17,17 @@ package ghidra.framework.project.extensions;
import java.awt.Font;
import java.awt.Point;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.JViewport;
import docking.widgets.table.threaded.ThreadedTableModelListener;
import generic.theme.*;
import ghidra.framework.plugintool.dialog.AbstractDetailsPanel;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.extensions.ExtensionDetails;
/**
@@ -43,6 +50,11 @@ class ExtensionDetailsPanel extends AbstractDetailsPanel {
private static final GColor FG_COLOR_VERSION =
new GColor("color.fg.extensionpanel.details.version");
private static final GColor FG_COLOR_CLASSES_HEADER =
new GColor("color.fg.extensionpanel.details.classes.header");
private static final GColor FG_COLOR_CLASSES_TYPE =
new GColor("color.fg.extensionpanel.details.classes.type");
/** Attribute sets define the visual characteristics for each field */
private GAttributes nameAttrSet;
private GAttributes descrAttrSet;
@@ -50,7 +62,11 @@ class ExtensionDetailsPanel extends AbstractDetailsPanel {
private GAttributes createdOnAttrSet;
private GAttributes versionAttrSet;
private GAttributes pathAttrSet;
private ExtensionDetails currentDetails;
private GAttributes classesHeaderAttrSet;
private GAttributes classesTypeAtrrSet;
private ExtensionRowObject currentRowObject;
ExtensionDetailsPanel(ExtensionTablePanel tablePanel) {
createFieldAttributes();
@@ -83,23 +99,30 @@ class ExtensionDetailsPanel extends AbstractDetailsPanel {
@Override
protected void refresh() {
setDescription(currentDetails);
JViewport vp = sp.getViewport();
Point p = vp.getViewPosition();
setDescription(currentRowObject);
// restore the viewer's scrolled position to avoid jumping around
vp.setViewPosition(p);
}
/**
* Updates this panel with the given extension.
*
* @param details the extension to display
*/
public void setDescription(ExtensionDetails details) {
private void setDescription(ExtensionRowObject ro) {
this.currentDetails = details;
this.currentRowObject = ro;
clear();
if (details == null) {
if (ro == null) {
return;
}
ExtensionDetails details = ro.getExtension();
StringBuilder buffer = new StringBuilder("<html>");
buffer.append("<H2>Extension Properties</H2>");
buffer.append("<TABLE cellpadding=2>");
insertRowTitle(buffer, "Name");
@@ -133,10 +156,75 @@ class ExtensionDetailsPanel extends AbstractDetailsPanel {
buffer.append("</TABLE>");
addExtensionClasses(buffer, ro);
textLabel.setText(buffer.toString());
sp.getViewport().setViewPosition(new Point(0, 0));
}
private void addExtensionClasses(StringBuilder buffer, ExtensionRowObject ro) {
Set<ClassFileInfo> infos = ro.getClassInfos();
if (infos.isEmpty()) {
return;
}
buffer.append("<BR><CENTER><HR></CENTER><BR>");
buffer.append("<H2>Provided Extension Points</H2>");
buffer.append("<TABLE cellpadding=4>");
buffer.append("<TR>");
insertHeader(buffer, "Type");
insertHeader(buffer, "Implementations");
buffer.append("</TR>");
Map<String, Set<ClassFileInfo>> classesByType = infos.stream()
.collect(
Collectors.groupingBy(
ClassFileInfo::suffix,
Collectors.toSet()));
Set<Entry<String, Set<ClassFileInfo>>> entries = classesByType.entrySet();
for (Entry<String, Set<ClassFileInfo>> entry : entries) {
String type = entry.getKey();
insertRowTitle(buffer, type, classesTypeAtrrSet);
StringBuilder infosBuffer = new StringBuilder();
Set<ClassFileInfo> typeInfos = entry.getValue();
for (ClassFileInfo typeInfo : typeInfos) {
String name = typeInfo.name();
String shortName = getShortName(name);
insertHTMLLine(infosBuffer, shortName, descrAttrSet);
}
buffer.append("<TD VALIGN=\"TOP\" WIDTH=\"80%\">");
buffer.append(infosBuffer.toString());
buffer.append("</TD>");
buffer.append("</TR>");
}
buffer.append("</TABLE>");
}
private String getShortName(String name) {
int index = name.lastIndexOf('.');
if (index < 0) {
return name; // no package
}
return name.substring(index + 1);
}
protected void insertHeader(StringBuilder buffer, String rowName) {
buffer.append("<TH VALIGN=\"TOP\" ALIGN=\"LEFT\">");
insertHTMLLine(buffer, rowName, classesHeaderAttrSet);
buffer.append("</TH>");
}
@Override
protected void createFieldAttributes() {
@@ -148,5 +236,8 @@ class ExtensionDetailsPanel extends AbstractDetailsPanel {
createdOnAttrSet = new GAttributes(font, FG_COLOR_DATE);
versionAttrSet = new GAttributes(font, FG_COLOR_VERSION);
pathAttrSet = new GAttributes(font, FG_COLOR_PATH);
classesHeaderAttrSet = new GAttributes(font, FG_COLOR_CLASSES_HEADER);
classesTypeAtrrSet = new GAttributes(font, FG_COLOR_CLASSES_TYPE);
}
}
@@ -0,0 +1,105 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.project.extensions;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
/**
* A class that contains an {@link ExtensionDetails} and any {@link ClassFileInfo extension points}
* loaded from that extension.
*/
public class ExtensionInstallationInfo {
private ExtensionDetails extension;
private Set<ClassFileInfo> classInfos = new HashSet<>();
/**
* {@return information for each installed extension}
*/
public static Set<ExtensionInstallationInfo> get() {
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
return loadExtensionPointInfo(extensions);
}
private ExtensionInstallationInfo(ExtensionDetails extension) {
this.extension = extension;
}
private static Set<ExtensionInstallationInfo> loadExtensionPointInfo(
Set<ExtensionDetails> extensions) {
// Map all class infos by module so we can then do one lookup per extension. Standardize on
// forward slashes for consistency.
Set<ClassFileInfo> extensionPoints = ClassSearcher.getExtensionPointInfo();
Map<String, List<ClassFileInfo>> classesByModule = extensionPoints.stream()
.collect(Collectors.groupingBy(ClassFileInfo::module));
Set<ExtensionInstallationInfo> results = new HashSet<>();
for (ExtensionDetails extension : extensions) {
ExtensionInstallationInfo info = new ExtensionInstallationInfo(extension);
results.add(info);
File installDir = extension.getInstallDir();
String path = installDir.getAbsolutePath();
List<ClassFileInfo> classes = classesByModule.get(path);
if (classes != null) {
info.classInfos.addAll(classes);
}
}
return results;
}
public ExtensionDetails getExtension() {
return extension;
}
public Set<ClassFileInfo> getClassInfos() {
return classInfos;
}
@Override
public String toString() {
return extension.toString();
}
@Override
public int hashCode() {
return Objects.hash(extension);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ExtensionInstallationInfo other = (ExtensionInstallationInfo) obj;
return Objects.equals(extension, other.extension);
}
}
@@ -46,7 +46,7 @@ import utility.application.ApplicationLayout;
*
* <p>
* Extensions may be installed/uninstalled by users at runtime, using the
* {@link ExtensionTableProvider}. Installation consists of unzipping the extension archive to an
* {@link ExtensionTableDialog}. Installation consists of unzipping the extension archive to an
* installation folder, currently <code>{ghidra user settings dir}/Extensions</code>. To uninstall,
* the unpacked folder is simply removed.
*/
@@ -0,0 +1,74 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.project.extensions;
import java.util.Objects;
import java.util.Set;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.extensions.ExtensionDetails;
class ExtensionRowObject {
private ExtensionDetails extension;
private ExtensionInstallationInfo info;
ExtensionRowObject(ExtensionDetails extension) {
this.extension = Objects.requireNonNull(extension);
}
ExtensionRowObject(ExtensionDetails extension, ExtensionInstallationInfo info) {
this(extension);
this.info = info;
}
public ExtensionDetails getExtension() {
return extension;
}
public Set<ClassFileInfo> getClassInfos() {
if (info == null) {
return Set.of(); // not installed
}
return info.getClassInfos();
}
@Override
public String toString() {
return extension.toString();
}
@Override
public int hashCode() {
return Objects.hash(extension);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ExtensionRowObject other = (ExtensionRowObject) obj;
return Objects.equals(extension, other.extension);
}
}
@@ -42,7 +42,7 @@ import resources.Icons;
* Component Provider that shows the known extensions in Ghidra in a {@link GTable}. Users may
* install/uninstall extensions, or add new ones.
*/
public class ExtensionTableProvider extends DialogComponentProvider {
public class ExtensionTableDialog extends DialogComponentProvider {
private static final String LAST_IMPORT_DIRECTORY_KEY = "LastExtensionImportDirectory";
@@ -55,7 +55,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
*
* @param tool the plugin tool
*/
public ExtensionTableProvider(PluginTool tool) {
public ExtensionTableDialog(PluginTool tool) {
super("Install Extensions");
addWorkPanel(createMainPanel(tool));
setHelpLocation(new HelpLocation(GenericHelpTopics.FRONT_END, "Extensions"));
@@ -73,12 +73,11 @@ public class ExtensionTableProvider extends DialogComponentProvider {
extensionTablePanel = new ExtensionTablePanel(tool);
extensionTablePanel.getAccessibleContext().setAccessibleName("Extenstion Table");
ExtensionDetailsPanel extensionDetailsPanel =
new ExtensionDetailsPanel(extensionTablePanel);
extensionDetailsPanel.getAccessibleContext().setAccessibleName("Extension Details");
ExtensionDetailsPanel detailsPanel = new ExtensionDetailsPanel(extensionTablePanel);
detailsPanel.getAccessibleContext().setAccessibleName("Extension Details");
final JSplitPane splitPane =
new JSplitPane(JSplitPane.VERTICAL_SPLIT, extensionTablePanel, extensionDetailsPanel);
JSplitPane splitPane =
new JSplitPane(JSplitPane.VERTICAL_SPLIT, extensionTablePanel, detailsPanel);
splitPane.setResizeWeight(.75);
splitPane.getAccessibleContext().setAccessibleName("Extension Table and Details");
panel.add(splitPane, BorderLayout.CENTER);
@@ -86,7 +85,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
splitPane.setDividerLocation(.75);
createAddAction(extensionTablePanel);
createRefreshAction(extensionTablePanel, extensionDetailsPanel);
createRefreshAction(extensionTablePanel, detailsPanel);
addOKButton();
panel.getAccessibleContext().setAccessibleName("Extension Table Provider");
@@ -129,41 +128,12 @@ public class ExtensionTableProvider extends DialogComponentProvider {
return false;
}
Object contextObject = context.getContextObject();
return ExtensionTableProvider.this == contextObject;
return ExtensionTableDialog.this == contextObject;
}
@Override
public void actionPerformed(ActionContext context) {
// Don't let the user attempt to install anything if they don't have write
// permissions on the installation dir.
ResourceFile installDir =
Application.getApplicationLayout().getExtensionInstallationDirs().get(0);
if (!installDir.exists() && !installDir.mkdir()) {
Msg.showError(this, null, "Directory Error",
"Cannot install/uninstall extensions: Failed to create extension " +
"installation directory: " + installDir);
}
if (!installDir.canWrite()) {
Msg.showError(this, null, "Permissions Error",
"Cannot install/uninstall extensions: Invalid write permissions on " +
"installation directory: " + installDir);
return;
}
GhidraFileChooser chooser = new GhidraFileChooser(getComponent());
chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_AND_DIRECTORIES);
chooser.setLastDirectoryPreference(LAST_IMPORT_DIRECTORY_KEY);
chooser.setTitle("Select Extension");
chooser.addFileFilter(new ExtensionFileFilter());
List<File> files = chooser.getSelectedFiles();
chooser.dispose();
if (installExtensions(files)) {
panel.refreshTable();
requireRestart = true;
}
doAddExtension(panel);
}
};
@@ -175,6 +145,38 @@ public class ExtensionTableProvider extends DialogComponentProvider {
addAction(addAction);
}
private void doAddExtension(ExtensionTablePanel panel) {
// Don't let the user attempt to install anything if they don't have write
// permissions on the installation dir.
List<ResourceFile> dirs = Application.getApplicationLayout().getExtensionInstallationDirs();
ResourceFile installDir = dirs.get(0);
if (!installDir.exists() && !installDir.mkdir()) {
Msg.showError(this, null, "Directory Error",
"Cannot install/uninstall extensions: Failed to create extension " +
"installation directory: " + installDir);
}
if (!installDir.canWrite()) {
Msg.showError(this, null, "Permissions Error",
"Cannot install/uninstall extensions: Invalid write permissions on " +
"installation directory: " + installDir);
return;
}
GhidraFileChooser chooser = new GhidraFileChooser(getComponent());
chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_AND_DIRECTORIES);
chooser.setLastDirectoryPreference(LAST_IMPORT_DIRECTORY_KEY);
chooser.setTitle("Select Extension");
chooser.addFileFilter(new ExtensionFileFilter());
List<File> files = chooser.getSelectedFiles();
chooser.dispose();
if (installExtensions(files)) {
panel.refreshTable();
requireRestart = true;
}
}
private boolean installExtensions(List<File> files) {
boolean didInstall = false;
for (File file : files) {
@@ -183,9 +185,9 @@ public class ExtensionTableProvider extends DialogComponentProvider {
// instead of a fully built extension.
if (new File(file, "build.gradle").isFile()) {
Msg.showWarn(this, null, "Invalid Extension",
"The selected extension " +
"contains a 'build.gradle' file.\nGhidra does not support installing " +
"extensions in source form.\nPlease build the extension and try again.");
"The selected extension contains a 'build.gradle' file.\n" +
"Ghidra does not support installing extensions in source form.\n" +
"Please build the extension and try again.");
continue;
}
@@ -196,7 +198,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
}
/**
* Creates an action to refresh the extensions list.
* Creates an action to refresh the extensions list.
*
* @param tablePanel the table to be refreshed
* @param detailsPanel the details to be refreshed
@@ -210,7 +212,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
@Override
public boolean isEnabledForContext(ActionContext context) {
Object contextObject = context.getContextObject();
return ExtensionTableProvider.this == contextObject;
return ExtensionTableDialog.this == contextObject;
}
@Override
@@ -37,18 +37,19 @@ import ghidra.util.task.TaskMonitor;
* Model for the {@link ExtensionTablePanel}. This defines 5 columns for displaying information in
* {@link ExtensionDetails} objects:
* <pre>
* - Installed (checkbox)
* - Installation Status
* - Name
* - Description
* - Installation directory (hidden)
* - Archive directory (hidden)
* - Version
* - Installation Directory (hidden)
* - Archive File (hidden)
* </pre>
* <p>
* All columns are for display purposes only, except for the <code>installed</code> column, which
* is a checkbox allowing users to install/uninstall a particular extension.
*
*/
class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
class ExtensionTableModel extends ThreadedTableModel<ExtensionRowObject, Object> {
/** We don't care about the ordering of other columns, but the install/uninstall checkbox should be
the first one and the name col is our initial sort column. */
@@ -56,7 +57,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
final static int NAME_COL = 1;
/** This is the data source for the model. Whatever is here will be displayed in the table. */
private Set<ExtensionDetails> extensions;
private Set<ExtensionRowObject> extensions;
private Map<String, Boolean> originalInstallStates = new HashMap<>();
/**
@@ -69,9 +70,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
protected TableColumnDescriptor<ExtensionDetails> createTableColumnDescriptor() {
protected TableColumnDescriptor<ExtensionRowObject> createTableColumnDescriptor() {
TableColumnDescriptor<ExtensionDetails> descriptor = new TableColumnDescriptor<>();
TableColumnDescriptor<ExtensionRowObject> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new ExtensionInstalledColumn(), INSTALLED_COL, true);
descriptor.addVisibleColumn(new ExtensionNameColumn(), NAME_COL, true);
@@ -111,27 +112,23 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
// We only care about the install column here, as it's the only one that
// is editable.
// We only care about the install column here, as it's the only one that is editable.
if (columnIndex != INSTALLED_COL) {
return;
}
// If the user does not have write permissions on the installation dir, they cannot
// install.
ResourceFile installDir =
Application.getApplicationLayout().getExtensionInstallationDirs().get(0);
// If the user does not have write permissions on the installation dir, they cannot install.
List<ResourceFile> dirs = Application.getApplicationLayout().getExtensionInstallationDirs();
ResourceFile installDir = dirs.get(0);
if (!installDir.exists() && !installDir.mkdir()) {
Msg.showError(this, null, "Directory Error",
"Cannot install/uninstall extensions: Failed to create extension installation " +
"directory.\nSee the \"Ghidra Extension Notes\" section of the Ghidra " +
"Installation Guide for more information.");
"Cannot install/uninstall extensions: Failed to create installation directory.\n" +
"See the 'Ghidra Extension Notes' section of the Ghidra Installation Guide.");
}
if (!installDir.canWrite()) {
Msg.showError(this, null, "Permissions Error",
"Cannot install/uninstall extensions: Invalid write permissions on installation " +
"directory.\nSee the \"Ghidra Extension Notes\" section of the Ghidra " +
"Installation Guide for more information.");
"Cannot install/uninstall extensions: Cannot write to installation directory.\n" +
"See the 'Ghidra Extension Notes' section of the Ghidra Installation Guide.");
return;
}
@@ -165,8 +162,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
// This is a programming error
Msg.error(this,
"Unable install an extension that no longer exists. Restart Ghidra and " +
"try manually installing the extension: '" + extension.getName() + "'");
"Unable install an extension that no longer exists.\n" +
"Restart Ghidra and try manually installing the extension: '" +
extension.getName() + "'");
}
/**
@@ -187,7 +185,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
protected void doLoad(Accumulator<ExtensionDetails> accumulator, TaskMonitor monitor)
protected void doLoad(Accumulator<ExtensionRowObject> accumulator, TaskMonitor monitor)
throws CancelledException {
if (extensions != null) {
accumulator.addAll(extensions);
@@ -196,22 +194,29 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
ExtensionUtils.reload();
Set<ExtensionDetails> archived = ExtensionUtils.getArchiveExtensions();
Set<ExtensionDetails> installed = ExtensionUtils.getInstalledExtensions();
// don't show archived extensions that have been installed
for (ExtensionDetails extension : installed) {
if (archived.remove(extension)) {
Msg.trace(this,
"Not showing archived extension that has been installed. Archive path: " +
extension.getArchivePath()); // useful for debugging
}
}
Set<ExtensionInstallationInfo> installed = ExtensionInstallationInfo.get();
extensions = new HashSet<>();
extensions.addAll(installed);
extensions.addAll(archived);
for (ExtensionDetails e : extensions) {
// don't show archived extensions that have been installed
for (ExtensionInstallationInfo info : installed) {
ExtensionDetails e = info.getExtension();
if (archived.remove(e)) {
Msg.trace(this,
"Not showing archived extension that has been installed. Archive path: " +
e.getArchivePath()); // useful for debugging
}
extensions.add(new ExtensionRowObject(e, info));
}
for (ExtensionDetails e : archived) {
extensions.add(new ExtensionRowObject(e));
}
for (ExtensionRowObject ro : extensions) {
ExtensionDetails e = ro.getExtension();
String name = e.getName();
if (originalInstallStates.containsKey(name)) {
continue; // preserve the original value
@@ -229,7 +234,8 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
*/
public boolean hasModelChanged() {
for (ExtensionDetails e : extensions) {
for (ExtensionRowObject ro : extensions) {
ExtensionDetails e = ro.getExtension();
Boolean wasInstalled = originalInstallStates.get(e.getName());
if (wasInstalled == null) {
return false;
@@ -248,7 +254,12 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* @param model the list to use as the model
*/
public void setModelData(List<ExtensionDetails> model) {
extensions = new HashSet<>(model);
extensions = new HashSet<>();
for (ExtensionDetails e : model) {
extensions.add(new ExtensionRowObject(e));
}
reload();
}
@@ -270,14 +281,18 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* @return the selected extension, or null if nothing is selected
*/
private ExtensionDetails getSelectedExtension(int row) {
return getRowObject(row);
ExtensionRowObject ro = getRowObject(row);
if (ro != null) {
return ro.getExtension();
}
return null;
}
/**
* Table column for displaying the extension name.
*/
private class ExtensionNameColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
extends AbstractDynamicTableColumn<ExtensionRowObject, String, Object> {
private ExtRenderer renderer = new ExtRenderer();
@@ -292,9 +307,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
public String getValue(ExtensionDetails rowObject, Settings settings, Object data,
public String getValue(ExtensionRowObject rowObject, Settings settings, Object data,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getName();
return rowObject.getExtension().getName();
}
@Override
@@ -307,7 +322,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* Table column for displaying the extension description.
*/
private class ExtensionDescriptionColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
extends AbstractDynamicTableColumn<ExtensionRowObject, String, Object> {
private ExtRenderer renderer = new ExtRenderer();
@@ -322,9 +337,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
public String getValue(ExtensionDetails rowObject, Settings settings, Object data,
public String getValue(ExtensionRowObject rowObject, Settings settings, Object data,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getDescription();
return rowObject.getExtension().getDescription();
}
@Override
@@ -337,7 +352,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* Table column for displaying the extension description.
*/
private class ExtensionVersionColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
extends AbstractDynamicTableColumn<ExtensionRowObject, String, Object> {
private ExtRenderer renderer = new ExtRenderer();
@@ -352,10 +367,10 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
public String getValue(ExtensionDetails rowObject, Settings settings, Object data,
public String getValue(ExtensionRowObject rowObject, Settings settings, Object data,
ServiceProvider sp) throws IllegalArgumentException {
String version = rowObject.getVersion();
String version = rowObject.getExtension().getVersion();
// Check for the default version value. If this is still set, then no version has been
// established so just display an empty string.
@@ -376,7 +391,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* Table column for displaying the extension installation status.
*/
private class ExtensionInstalledColumn
extends AbstractDynamicTableColumn<ExtensionDetails, Boolean, Object> {
extends AbstractDynamicTableColumn<ExtensionRowObject, Boolean, Object> {
@Override
public String getColumnName() {
@@ -389,9 +404,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
public Boolean getValue(ExtensionDetails rowObject, Settings settings, Object data,
public Boolean getValue(ExtensionRowObject rowObject, Settings settings, Object data,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.isInstalled();
return rowObject.getExtension().isInstalled();
}
}
@@ -399,7 +414,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* Table column for displaying the extension installation directory.
*/
private class ExtensionInstallationDirColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
extends AbstractDynamicTableColumn<ExtensionRowObject, String, Object> {
@Override
public String getColumnName() {
@@ -412,9 +427,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
public String getValue(ExtensionDetails rowObject, Settings settings, Object data,
public String getValue(ExtensionRowObject rowObject, Settings settings, Object data,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getInstallPath();
return rowObject.getExtension().getInstallPath();
}
}
@@ -422,7 +437,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* Table column for displaying the extension archive file.
*/
private class ExtensionArchiveFileColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
extends AbstractDynamicTableColumn<ExtensionRowObject, String, Object> {
@Override
public String getColumnName() {
@@ -435,9 +450,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
@Override
public String getValue(ExtensionDetails rowObject, Settings settings, Object data,
public String getValue(ExtensionRowObject rowObject, Settings settings, Object data,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getArchivePath();
return rowObject.getExtension().getArchivePath();
}
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -36,7 +36,7 @@ import help.HelpService;
*/
public class ExtensionTablePanel extends JPanel {
private GTableFilterPanel<ExtensionDetails> tableFilterPanel;
private GTableFilterPanel<ExtensionRowObject> tableFilterPanel;
private ExtensionTableModel tableModel;
private GTable table;
@@ -85,7 +85,7 @@ public class ExtensionTablePanel extends JPanel {
return table;
}
public ExtensionDetails getSelectedItem() {
public ExtensionRowObject getSelectedItem() {
return tableFilterPanel.getSelectedItem();
}
@@ -96,15 +96,6 @@ public class ExtensionTablePanel extends JPanel {
tableModel.refreshTable();
}
/**
* Returns the filter panel.
*
* @return the filter panel
*/
public GTableFilterPanel<ExtensionDetails> getFilterPanel() {
return tableFilterPanel;
}
/**
* Replaces the contents of the table with the given list of extensions.
*
@@ -18,6 +18,8 @@ package ghidra.framework.project.tool;
import java.util.Map;
import java.util.Set;
import ghidra.util.classfinder.ClassFileInfo;
/**
* An interface to help describe extensions' enable state for a given tool.
*/
@@ -26,18 +28,18 @@ public interface ExtensionsEnabledState {
/**
* {@return a map of all known extensions to a set of their plugins}
*/
public Map<String, Set<Class<?>>> getAllKnownExtensions();
public Map<String, Set<ClassFileInfo>> getAllKnownExtensions();
/**
* All plugins installed in the current tool will be removed from the given set. This allows the
* client to have a set of plugins that are not currently installed.
* @param allPlugins the plugins set to update
*/
public void removeInstalledPlugins(Set<Class<?>> allPlugins);
public void removeInstalledPlugins(Set<ClassFileInfo> allPlugins);
/**
* Shows a window to prompt the user to configure any new extension plugins.
* @param newPlugins the new extension plugins
*/
public void propmtToConfigureNewPlugins(Set<Class<?>> newPlugins);
public void propmtToConfigureNewPlugins(Set<ClassFileInfo> newPlugins);
}
@@ -16,26 +16,25 @@
package ghidra.framework.project.tool;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
import docking.widgets.OptionDialog;
import generic.json.Json;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.dialog.PluginInstallerDialog;
import ghidra.framework.plugintool.util.PluginDescription;
import ghidra.framework.project.extensions.ExtensionInstallationInfo;
import ghidra.util.SystemUtilities;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import utilities.util.FileUtilities;
/**
* The default extension state for a {@link PluginTool}.
*/
class ToolExtensionsEnabledState implements ExtensionsEnabledState {
private static String PLUGIN_SUFFIX = Plugin.class.getSimpleName();
private PluginTool tool;
ToolExtensionsEnabledState(PluginTool tool) {
@@ -43,33 +42,61 @@ class ToolExtensionsEnabledState implements ExtensionsEnabledState {
}
@Override
public Map<String, Set<Class<?>>> getAllKnownExtensions() {
public Map<String, Set<ClassFileInfo>> getAllKnownExtensions() {
Set<ExtensionDetails> extensions = getExtensions();
Set<ExtensionInstallationInfo> extensions = getExtensions();
if (extensions.isEmpty()) {
return Map.of();
}
Map<String, Set<Class<?>>> plugins = new HashMap<>();
Set<PluginPath> pluginPaths = getAllPluginPaths();
for (ExtensionDetails extension : extensions) {
Set<Class<?>> classes = findPluginsLoadedFromExtension(extension, pluginPaths);
plugins.put(extension.getName(), classes);
Map<String, Set<ClassFileInfo>> result = new HashMap<>();
for (ExtensionInstallationInfo info : extensions) {
ExtensionDetails extension = info.getExtension();
Set<ClassFileInfo> plugins = getPlugins(info);
if (plugins.isEmpty()) {
continue;
}
result.put(extension.getName(), plugins);
}
return plugins;
return result;
}
private Set<ClassFileInfo> getPlugins(ExtensionInstallationInfo info) {
Set<ClassFileInfo> result = new HashSet<>();
Set<ClassFileInfo> classInfos = info.getClassInfos();
for (ClassFileInfo classInfo : classInfos) {
String suffix = classInfo.suffix();
if (PLUGIN_SUFFIX.equals(suffix)) {
result.add(classInfo);
}
}
return result;
}
@Override
public void removeInstalledPlugins(Set<Class<?>> plugins) {
public void removeInstalledPlugins(Set<ClassFileInfo> plugins) {
List<Plugin> activePlugins = tool.getManagedPlugins();
for (Plugin plugin : activePlugins) {
Class<? extends Plugin> clazz = plugin.getClass();
plugins.remove(clazz);
Set<String> activeClassNames = activePlugins.stream()
.map(Plugin::getClass)
.map(Class::getName)
.collect(Collectors.toSet());
Iterator<ClassFileInfo> it = plugins.iterator();
while (it.hasNext()) {
ClassFileInfo info = it.next();
String name = info.name();
if (activeClassNames.contains(name)) {
it.remove();
}
}
}
@Override
public void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
public void propmtToConfigureNewPlugins(Set<ClassFileInfo> plugins) {
// Offer the user a chance to configure any newly discovered plugins
int option = OptionDialog.showYesNoDialog(tool.getToolFrame(), "New Plugins Found!",
"New extension plugins detected. Would you like to configure them?");
@@ -81,20 +108,20 @@ class ToolExtensionsEnabledState implements ExtensionsEnabledState {
}
}
private static Set<PluginPath> getAllPluginPaths() {
Set<PluginPath> paths = new HashSet<>();
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
for (Class<? extends Plugin> plugin : plugins) {
paths.add(new PluginPath(plugin));
}
return paths;
}
private static Set<ExtensionInstallationInfo> getExtensions() {
private static Set<ExtensionDetails> getExtensions() {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
return installedExtensions.stream()
.filter(e -> !isRepoExtension(e))
.collect(Collectors.toSet());
Set<ExtensionInstallationInfo> infos = ExtensionInstallationInfo.get();
Iterator<ExtensionInstallationInfo> it = infos.iterator();
while (it.hasNext()) {
ExtensionInstallationInfo info = it.next();
ExtensionDetails e = info.getExtension();
if (isRepoExtension(e) || e.isPendingUninstall()) {
it.remove();
}
}
return infos;
}
/**
@@ -117,45 +144,6 @@ class ToolExtensionsEnabledState implements ExtensionsEnabledState {
return false;
}
/**
* Finds all plugin classes loaded from a particular extension folder.
* <p>
* This uses the {@link ClassSearcher} to find all <code>Plugin.class</code> objects on the
* classpath. For each class, the original resource file is compared against the
* given extension folder and the jar files for that extension.
*
* @param extension the extension from which to find plugins
* @param pluginPaths all loaded plugin paths
* @return list of {@link Plugin} classes, or empty list if none found
*/
private static Set<Class<?>> findPluginsLoadedFromExtension(ExtensionDetails extension,
Set<PluginPath> pluginPaths) {
if (!extension.isInstalled()) {
return Collections.emptySet();
}
// Find any jar files in the directory provided
Set<URL> jarPaths = extension.getLibraries();
// Now get all Plugin.class file paths and see if any of them were loaded from one of the
// extension the given extension directory
Set<Class<?>> result = new HashSet<>();
for (PluginPath pluginPath : pluginPaths) {
if (pluginPath.isFrom(extension.getInstallDir())) {
result.add(pluginPath.getPluginClass());
continue;
}
for (URL jarUrl : jarPaths) {
if (pluginPath.isFrom(jarUrl)) {
result.add(pluginPath.getPluginClass());
}
}
}
return result;
}
/**
* Finds all {@link PluginDescription} objects that match a given set of plugin classes. This
* effectively tells the caller which of the given plugins have been loaded by the class loader.
@@ -167,7 +155,7 @@ class ToolExtensionsEnabledState implements ExtensionsEnabledState {
* @param plugins the list of plugin classes to search for
* @return list of plugin descriptions
*/
private List<PluginDescription> getPluginDescriptions(Set<Class<?>> plugins) {
private List<PluginDescription> getPluginDescriptions(Set<ClassFileInfo> plugins) {
// First define the list of plugin descriptions to return
List<PluginDescription> descriptions = new ArrayList<>();
@@ -178,11 +166,11 @@ class ToolExtensionsEnabledState implements ExtensionsEnabledState {
pluginsConfiguration.getManagedPluginDescriptions();
// see if an entry exists in the list of all loaded plugins
for (Class<?> plugin : plugins) {
String pluginName = plugin.getSimpleName();
for (ClassFileInfo info : plugins) {
String pluginName = info.simpleName();
Optional<PluginDescription> desc = allPluginDescriptions.stream()
.filter(d -> (pluginName.equals(d.getName())))
.filter(d -> pluginName.equals(d.getName()))
.findAny();
if (desc.isPresent()) {
descriptions.add(desc.get());
@@ -192,35 +180,4 @@ class ToolExtensionsEnabledState implements ExtensionsEnabledState {
return descriptions;
}
private static class PluginPath {
private Class<? extends Plugin> pluginClass;
private String pluginLocation;
private File pluginFile;
PluginPath(Class<? extends Plugin> pluginClass) {
this.pluginClass = pluginClass;
String name = pluginClass.getName();
URL url = pluginClass.getResource('/' + name.replace('.', '/') + ".class");
this.pluginLocation = url.getPath();
this.pluginFile = new File(pluginLocation);
}
public boolean isFrom(File dir) {
return FileUtilities.isPathContainedWithin(dir, pluginFile);
}
boolean isFrom(URL jarUrl) {
String jarPath = jarUrl.getPath();
return pluginLocation.contains(jarPath);
}
Class<? extends Plugin> getPluginClass() {
return pluginClass;
}
@Override
public String toString() {
return Json.toString(this);
}
}
}
@@ -25,6 +25,7 @@ import org.jdom2.Element;
import ghidra.util.NumericUtilities;
import ghidra.util.Swing;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.xml.XmlUtilities;
/**
@@ -39,7 +40,7 @@ class ToolExtensionsStatusManager {
private static final String XML_ATTR_EXTENSION_NAME = "NAME";
private static final String XML_ATTR_EXTENSION_PLUGIN_CLASS = "CLASS";
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
private Set<ClassFileInfo> newExtensionPlugins = new HashSet<>();
private ExtensionsEnabledState extensionsState;
ToolExtensionsStatusManager(ExtensionsEnabledState extensionState) {
@@ -60,21 +61,21 @@ class ToolExtensionsStatusManager {
void saveToXml(Element xml) {
Map<String, Set<Class<?>>> pluginsByExtension =
Map<String, Set<ClassFileInfo>> pluginsByExtension =
extensionsState.getAllKnownExtensions();
Element extensionsParent = new Element(XML_TAG_EXTENSIONS);
Set<Entry<String, Set<Class<?>>>> entries = pluginsByExtension.entrySet();
for (Entry<String, Set<Class<?>>> entry : entries) {
Set<Entry<String, Set<ClassFileInfo>>> entries = pluginsByExtension.entrySet();
for (Entry<String, Set<ClassFileInfo>> entry : entries) {
String name = entry.getKey();
Element extensionsElement = new Element(XML_TAG_EXTENSION);
setExtensionName(extensionsElement, name);
extensionsParent.addContent(extensionsElement);
Set<Class<?>> plugins = entry.getValue();
for (Class<?> clazz : plugins) {
String className = clazz.getName();
Set<ClassFileInfo> infos = entry.getValue();
for (ClassFileInfo info : infos) {
String className = info.name();
Element pluginElement = new Element(XML_TAG_PLUGIN);
pluginElement.setAttribute(XML_ATTR_EXTENSION_PLUGIN_CLASS, className);
extensionsElement.addContent(pluginElement);
@@ -98,7 +99,7 @@ class ToolExtensionsStatusManager {
5) Save the new extension plugins for later user prompting when saving to xml.
*/
Map<String, Set<Class<?>>> extensionPlugins = extensionsState.getAllKnownExtensions();
Map<String, Set<ClassFileInfo>> extensionPlugins = extensionsState.getAllKnownExtensions();
Map<String, ExtensionMemento> xmlMementosByName = getKnownExtensions(xml);
Set<String> names = extensionPlugins.keySet();
Set<String> newExtensions = new HashSet<>(names);
@@ -115,7 +116,7 @@ class ToolExtensionsStatusManager {
}
}
Set<Class<?>> newPlugins = newExtensions.stream()
Set<ClassFileInfo> newPlugins = newExtensions.stream()
.map(name -> extensionPlugins.get(name)) // classes by extension name
.flatMap(set -> set.stream()) // map all sets to a single stream
.collect(Collectors.toSet());
@@ -129,19 +130,19 @@ class ToolExtensionsStatusManager {
}
private static boolean hasNewPlugins(ExtensionMemento xmlMemento,
Map<String, Set<Class<?>>> pluginsByExtensionName) {
Map<String, Set<ClassFileInfo>> pluginsByExtensionName) {
// If the xml memento is empty, it is either the old style xml that did not save plugin
// names or is the new style xml, but the extension did not previously have any plugins. In
// this case, we want to prompt the user if there are plugins to install.
Set<String> xmlClassNames = xmlMemento.pluginClassNames();
Set<Class<?>> cpPluginClasses = pluginsByExtensionName.get(xmlMemento.name());
Set<ClassFileInfo> cpPluginClasses = pluginsByExtensionName.get(xmlMemento.name());
if (xmlClassNames.isEmpty() && !cpPluginClasses.isEmpty()) {
return true;
}
List<String> cpNames = cpPluginClasses.stream()
.map(c -> c.getName())
.map(c -> c.name())
.collect(Collectors.toList());
cpNames.removeAll(xmlClassNames);
@@ -43,7 +43,7 @@ import ghidra.framework.main.wizard.project.*;
import ghidra.framework.model.*;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.project.extensions.ExtensionTablePanel;
import ghidra.framework.project.extensions.ExtensionTableProvider;
import ghidra.framework.project.extensions.ExtensionTableDialog;
import ghidra.framework.remote.User;
import ghidra.framework.store.LockException;
import ghidra.program.database.ProgramContentHandler;
@@ -190,8 +190,8 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
performAction("Extensions", "Project Window", false);
waitForSwing();
ExtensionTableProvider provider =
(ExtensionTableProvider) getDialog(ExtensionTableProvider.class);
ExtensionTableDialog provider =
(ExtensionTableDialog) getDialog(ExtensionTableDialog.class);
Object panel = getInstanceField("extensionTablePanel", provider);
GTable table = (GTable) getInstanceField("table", panel);