diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm index 98ca7125c4..9e27b3c4c4 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Extensions.htm @@ -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.
-Ghidra Extensions can be installed and uninstalled at runtime, with the changes taking effect - when Ghidra is restarted. The extension installation dialog can +
Ghidra Extensions can be installed and uninstalled at runtime, with the changes taking effect + when Ghidra is restarted. The extension installation dialog can be opened by selecting the Install Extensions option on the project File menu.
@@ -32,11 +32,61 @@+
++ +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 not 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 + File
+ +Congfigure menu to + enable any plugins added by newly installed extensions. +
++ ++
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:
+
+- + Configure plugins via File
++ Congfigure +
- + Click the
+icon to show all plugins +
- + If not already visible, add the Module table column via the right-click + menu of the table header. (Extensions are modules.) +
+- + Sort on the Module table column +
+- + Optionally, you can type the name of the extension into the filter to hide other modules +
+
+ +To install an extension, select the extension's checkbox. To unininstall, deselect the + checkbox.
+ + +++ ++
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.
The list of extensions is populated when the dialog is launched. To build the list, Ghidra looks in several locations:
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm b/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm index 244f99b529..5357aec068 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/GhidraScriptMgrPlugin/GhidraScriptMgrPlugin.htm @@ -371,8 +371,8 @@diff --git a/Ghidra/Features/Base/src/main/help/help/topics/RuntimeInfoPlugin/RuntimeInfo.htm b/Ghidra/Features/Base/src/main/help/help/topics/RuntimeInfoPlugin/RuntimeInfo.htm index 4307f17bc3..df5910b41c 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/RuntimeInfoPlugin/RuntimeInfo.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/RuntimeInfoPlugin/RuntimeInfo.htm @@ -27,7 +27,8 @@
- Version - Ghidra, Operating System, and Java version information. Clicking the - Copy button will copy this information to the clipboard for easy transfer into a bug report. + Copy button will copy this information to the clipboard for easy transfer into + a bug report.
- Memory - JVM memory usage. Clicking the Collect Garbage button will @@ -47,7 +48,11 @@ Modules - A list of discovered Ghidra Modules.
- - Extension Points - A list of discovered Ghidra Extension Points. + Extension Points - A list of discovered Ghidra Extension Points.
+ 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. +
- Classpath - The ordered classpath for the active Ghidra application. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java index 25023aa8ce..f144e15a55 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java @@ -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
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); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java index 7d65a1b245..f7ce147905 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java @@ -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); } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/ExtensionManagerTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/ToolExtensionStatusManagerTest.java similarity index 90% rename from Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/ExtensionManagerTest.java rename to Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/ToolExtensionStatusManagerTest.java index 785481f9ef..930cc1be67 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/ExtensionManagerTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/framework/project/tool/ToolExtensionStatusManagerTest.java @@ -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 >> extensions = new HashMap<>(); - private Set > installedPlugins = new HashSet<>(); + private Map > extensions = new HashMap<>(); + private Set installedPlugins = new HashSet<>(); private boolean didPrompt = false; @Override - public Map >> getAllKnownExtensions() { + public Map > getAllKnownExtensions() { return extensions; } @Override - public void removeInstalledPlugins(Set > plugins) { + public void removeInstalledPlugins(Set plugins) { plugins.removeAll(installedPlugins); } @Override - public void propmtToConfigureNewPlugins(Set > plugins) { + public void propmtToConfigureNewPlugins(Set 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 c) { - installedPlugins.add(c); + + String name = c.getName(); + ClassFileInfo info = new ClassFileInfo("/fake/path", name, "Plugin", ""); + installedPlugins.add(info); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/SettableColorSwatchChooserPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/SettableColorSwatchChooserPanel.java index 23c59e1afd..8cb3caebde 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/options/editor/SettableColorSwatchChooserPanel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/options/editor/SettableColorSwatchChooserPanel.java @@ -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); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java index 95061dcff0..1c509726c7 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java @@ -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 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(); } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFileInfo.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFileInfo.java index 56a2ed86f5..7fdb093726 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFileInfo.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFileInfo.java @@ -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); + } +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java index aab723f325..723c40795d 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java @@ -52,12 +52,15 @@ class ClassJar implements ClassLocation { private static final Set USER_PLUGIN_PATHS = loadUserPluginPaths(); private Set 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() { diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java index 0383c7d2ec..8c86a31ef8 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java @@ -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 children = new HashSet<>(); - private File rootDir; + private ClassDir classDir; private File packageDir; private String packageName; private Set 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 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 list, TaskMonitor monitor) throws CancelledException { list.addAll(classes); - Iterator it = children.iterator(); - while (it.hasNext()) { + for (ClassPackage subPkg : children) { monitor.checkCancelled(); - ClassPackage subPkg = it.next(); subPkg.getClasses(list, monitor); } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java index 826b8f12c8..23d25f7554 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java @@ -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() diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java index f46e91b56f..d0afe1065d 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/extensions/ExtensionUtils.java @@ -120,6 +120,9 @@ public class ExtensionUtils { return success; } + /** + * {@return all installed extensions that are not marked for uninstall} + */ public static Set getActiveInstalledExtensions() { return getAllInstalledExtensions().getActiveExtensions(); } diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java index c04510fa90..18c8b20d87 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/ApplicationThemeManager.java @@ -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)); }); } diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/GColor.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/GColor.java index 55d16233e6..ecb2d761a8 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/theme/GColor.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/GColor.java @@ -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 diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeDefaults.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeDefaults.java index 5a8116fc1e..6882d352bb 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeDefaults.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/GThemeDefaults.java @@ -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); } } } diff --git a/Ghidra/Framework/Project/data/project.theme.properties b/Ghidra/Framework/Project/data/project.theme.properties index e2085fae9b..8289370d47 100644 --- a/Ghidra/Framework/Project/data/project.theme.properties +++ b/Ghidra/Framework/Project/data/project.theme.properties @@ -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 diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java index 178c825bb0..87ec0b360e 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginTool.java @@ -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)); } /** diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/AbstractDetailsPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/AbstractDetailsPanel.java index dd1a901332..4e2821f418 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/AbstractDetailsPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/AbstractDetailsPanel.java @@ -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(" "); - buffer.append(" "); - insertHTMLLine(buffer, rowName + ":", titleAttrs); + buffer.append(" "); + insertHTMLLine(buffer, rowName + ":", attributes); buffer.append(" "); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginDetailsPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginDetailsPanel.java index ff21c5e6d7..ede82a14be 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginDetailsPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginDetailsPanel.java @@ -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); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java index 045434ca75..534e56c355 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java @@ -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( diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/util/PluginDescription.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/util/PluginDescription.java index 3515ec80b5..db63e1d0f0 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/util/PluginDescription.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/util/PluginDescription.java @@ -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{ 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 { } /** - * 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 { 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 extDirs = layout.getExtensionInstallationDirs(); + return FileUtilities.isPathContainedWithin(extDirs, myLocation); + } + /** * Return the class of the plugin. * @return plugin class object diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionDetailsPanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionDetailsPanel.java index b9329c3744..94c74603e4 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionDetailsPanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionDetailsPanel.java @@ -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(""); + + buffer.append(" Extension Properties
"); + buffer.append(""); insertRowTitle(buffer, "Name"); @@ -133,10 +156,75 @@ class ExtensionDetailsPanel extends AbstractDetailsPanel { buffer.append("
"); + addExtensionClasses(buffer, ro); + textLabel.setText(buffer.toString()); sp.getViewport().setViewPosition(new Point(0, 0)); } + private void addExtensionClasses(StringBuilder buffer, ExtensionRowObject ro) { + + Setinfos = ro.getClassInfos(); + if (infos.isEmpty()) { + return; + } + + buffer.append("
"); + + buffer.append("Provided Extension Points
"); + + buffer.append(""); + + buffer.append("
"); + + } + + 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(""); + insertHeader(buffer, "Type"); + insertHeader(buffer, "Implementations"); + buffer.append(" "); + + Map> classesByType = infos.stream() + .collect( + Collectors.groupingBy( + ClassFileInfo::suffix, + Collectors.toSet())); + + Set >> entries = classesByType.entrySet(); + for (Entry > entry : entries) { + String type = entry.getKey(); + + insertRowTitle(buffer, type, classesTypeAtrrSet); + + StringBuilder infosBuffer = new StringBuilder(); + Set typeInfos = entry.getValue(); + for (ClassFileInfo typeInfo : typeInfos) { + String name = typeInfo.name(); + String shortName = getShortName(name); + insertHTMLLine(infosBuffer, shortName, descrAttrSet); + } + + buffer.append(" "); + buffer.append(infosBuffer.toString()); + buffer.append(" "); + buffer.append(""); + } + + buffer.append(""); + insertHTMLLine(buffer, rowName, classesHeaderAttrSet); + buffer.append(" "); + } + @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); } + } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstallationInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstallationInfo.java new file mode 100644 index 0000000000..cc960cbf66 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstallationInfo.java @@ -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 SetclassInfos = new HashSet<>(); + + /** + * {@return information for each installed extension} + */ + public static Set get() { + Set extensions = ExtensionUtils.getInstalledExtensions(); + return loadExtensionPointInfo(extensions); + } + + private ExtensionInstallationInfo(ExtensionDetails extension) { + this.extension = extension; + } + + private static Set loadExtensionPointInfo( + Set extensions) { + + // Map all class infos by module so we can then do one lookup per extension. Standardize on + // forward slashes for consistency. + Set extensionPoints = ClassSearcher.getExtensionPointInfo(); + Map > classesByModule = extensionPoints.stream() + .collect(Collectors.groupingBy(ClassFileInfo::module)); + + Set results = new HashSet<>(); + for (ExtensionDetails extension : extensions) { + + ExtensionInstallationInfo info = new ExtensionInstallationInfo(extension); + results.add(info); + File installDir = extension.getInstallDir(); + String path = installDir.getAbsolutePath(); + List classes = classesByModule.get(path); + if (classes != null) { + info.classInfos.addAll(classes); + } + } + + return results; + } + + public ExtensionDetails getExtension() { + return extension; + } + + public Set 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); + } +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java index 319bb26435..48ce015f12 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionInstaller.java @@ -46,7 +46,7 @@ import utility.application.ApplicationLayout; * * * 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
{ghidra user settings dir}/Extensions. To uninstall, * the unpacked folder is simply removed. */ diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionRowObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionRowObject.java new file mode 100644 index 0000000000..10e69ab0d6 --- /dev/null +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionRowObject.java @@ -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 SetgetClassInfos() { + 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); + } + +} diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableProvider.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableDialog.java similarity index 75% rename from Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableProvider.java rename to Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableDialog.java index 3164509eec..e47200d42a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableProvider.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableDialog.java @@ -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 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 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 files = chooser.getSelectedFiles(); + chooser.dispose(); + + if (installExtensions(files)) { + panel.refreshTable(); + requireRestart = true; + } + } + private boolean installExtensions(List 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 diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableModel.java index eace5cabd1..263102a6fd 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableModel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTableModel.java @@ -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: * - * - Installed (checkbox) + * - Installation Status * - Name * - Description - * - Installation directory (hidden) - * - Archive directory (hidden) + * - Version + * - Installation Directory (hidden) + * - Archive File (hidden) *** All columns are for display purposes only, except for the
installedcolumn, which * is a checkbox allowing users to install/uninstall a particular extension. * */ -class ExtensionTableModel extends ThreadedTableModel{ +class ExtensionTableModel extends ThreadedTableModel { /** 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 { 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 extensions; + private Set extensions; private Map originalInstallStates = new HashMap<>(); /** @@ -69,9 +70,9 @@ class ExtensionTableModel extends ThreadedTableModel { } @Override - protected TableColumnDescriptor createTableColumnDescriptor() { + protected TableColumnDescriptor createTableColumnDescriptor() { - TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); + TableColumnDescriptor 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 { @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 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 { // 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 { } @Override - protected void doLoad(Accumulator accumulator, TaskMonitor monitor) + protected void doLoad(Accumulator accumulator, TaskMonitor monitor) throws CancelledException { if (extensions != null) { accumulator.addAll(extensions); @@ -196,22 +194,29 @@ class ExtensionTableModel extends ThreadedTableModel { ExtensionUtils.reload(); Set archived = ExtensionUtils.getArchiveExtensions(); - Set 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 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 { */ 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 { * @param model the list to use as the model */ public void setModelData(List 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 { * @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 { + extends AbstractDynamicTableColumn { private ExtRenderer renderer = new ExtRenderer(); @@ -292,9 +307,9 @@ class ExtensionTableModel extends ThreadedTableModel { } @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 { * Table column for displaying the extension description. */ private class ExtensionDescriptionColumn - extends AbstractDynamicTableColumn { + extends AbstractDynamicTableColumn { private ExtRenderer renderer = new ExtRenderer(); @@ -322,9 +337,9 @@ class ExtensionTableModel extends ThreadedTableModel { } @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 { * Table column for displaying the extension description. */ private class ExtensionVersionColumn - extends AbstractDynamicTableColumn { + extends AbstractDynamicTableColumn { private ExtRenderer renderer = new ExtRenderer(); @@ -352,10 +367,10 @@ class ExtensionTableModel extends ThreadedTableModel { } @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 { * Table column for displaying the extension installation status. */ private class ExtensionInstalledColumn - extends AbstractDynamicTableColumn { + extends AbstractDynamicTableColumn { @Override public String getColumnName() { @@ -389,9 +404,9 @@ class ExtensionTableModel extends ThreadedTableModel { } @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 { * Table column for displaying the extension installation directory. */ private class ExtensionInstallationDirColumn - extends AbstractDynamicTableColumn { + extends AbstractDynamicTableColumn { @Override public String getColumnName() { @@ -412,9 +427,9 @@ class ExtensionTableModel extends ThreadedTableModel { } @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 { * Table column for displaying the extension archive file. */ private class ExtensionArchiveFileColumn - extends AbstractDynamicTableColumn { + extends AbstractDynamicTableColumn { @Override public String getColumnName() { @@ -435,9 +450,9 @@ class ExtensionTableModel extends ThreadedTableModel { } @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(); } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTablePanel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTablePanel.java index 68ca08ab90..07216093b5 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTablePanel.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/extensions/ExtensionTablePanel.java @@ -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 tableFilterPanel; + private GTableFilterPanel 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 getFilterPanel() { - return tableFilterPanel; - } - /** * Replaces the contents of the table with the given list of extensions. * diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ExtensionsEnabledState.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ExtensionsEnabledState.java index 936c4efae7..1ed5b04f3d 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ExtensionsEnabledState.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ExtensionsEnabledState.java @@ -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 >> getAllKnownExtensions(); + public Map > 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 > allPlugins); + public void removeInstalledPlugins(Set allPlugins); /** * Shows a window to prompt the user to configure any new extension plugins. * @param newPlugins the new extension plugins */ - public void propmtToConfigureNewPlugins(Set > newPlugins); + public void propmtToConfigureNewPlugins(Set newPlugins); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsEnabledState.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsEnabledState.java index 6f24ca7a03..b8b17abf4f 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsEnabledState.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsEnabledState.java @@ -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 >> getAllKnownExtensions() { + public Map > getAllKnownExtensions() { - Set extensions = getExtensions(); + Set extensions = getExtensions(); if (extensions.isEmpty()) { return Map.of(); } - Map >> plugins = new HashMap<>(); - Set pluginPaths = getAllPluginPaths(); - for (ExtensionDetails extension : extensions) { - Set > classes = findPluginsLoadedFromExtension(extension, pluginPaths); - plugins.put(extension.getName(), classes); + Map > result = new HashMap<>(); + for (ExtensionInstallationInfo info : extensions) { + + ExtensionDetails extension = info.getExtension(); + Set plugins = getPlugins(info); + if (plugins.isEmpty()) { + continue; + } + + result.put(extension.getName(), plugins); } - return plugins; + return result; + } + + private Set getPlugins(ExtensionInstallationInfo info) { + + Set result = new HashSet<>(); + Set 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 > plugins) { + public void removeInstalledPlugins(Set plugins) { List activePlugins = tool.getManagedPlugins(); - for (Plugin plugin : activePlugins) { - Class extends Plugin> clazz = plugin.getClass(); - plugins.remove(clazz); + + Set activeClassNames = activePlugins.stream() + .map(Plugin::getClass) + .map(Class::getName) + .collect(Collectors.toSet()); + + Iterator 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 > plugins) { + public void propmtToConfigureNewPlugins(Set 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 getAllPluginPaths() { - Set paths = new HashSet<>(); - List > plugins = ClassSearcher.getClasses(Plugin.class); - for (Class extends Plugin> plugin : plugins) { - paths.add(new PluginPath(plugin)); - } - return paths; - } + private static Set getExtensions() { - private static Set getExtensions() { - Set installedExtensions = ExtensionUtils.getActiveInstalledExtensions(); - return installedExtensions.stream() - .filter(e -> !isRepoExtension(e)) - .collect(Collectors.toSet()); + Set infos = ExtensionInstallationInfo.get(); + Iterator 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. - * - * This uses the {@link ClassSearcher} to find all
Plugin.classobjects 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> findPluginsLoadedFromExtension(ExtensionDetails extension, - Set pluginPaths) { - - if (!extension.isInstalled()) { - return Collections.emptySet(); - } - - // Find any jar files in the directory provided - Set 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 > 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 getPluginDescriptions(Set > plugins) { + private List getPluginDescriptions(Set plugins) { // First define the list of plugin descriptions to return List 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 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); - } - } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsStatusManager.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsStatusManager.java index 0553c8250f..e3b6654dd2 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsStatusManager.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/project/tool/ToolExtensionsStatusManager.java @@ -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 > newExtensionPlugins = new HashSet<>(); + private Set newExtensionPlugins = new HashSet<>(); private ExtensionsEnabledState extensionsState; ToolExtensionsStatusManager(ExtensionsEnabledState extensionState) { @@ -60,21 +61,21 @@ class ToolExtensionsStatusManager { void saveToXml(Element xml) { - Map >> pluginsByExtension = + Map > pluginsByExtension = extensionsState.getAllKnownExtensions(); Element extensionsParent = new Element(XML_TAG_EXTENSIONS); - Set >>> entries = pluginsByExtension.entrySet(); - for (Entry >> entry : entries) { + Set >> entries = pluginsByExtension.entrySet(); + for (Entry > entry : entries) { String name = entry.getKey(); Element extensionsElement = new Element(XML_TAG_EXTENSION); setExtensionName(extensionsElement, name); extensionsParent.addContent(extensionsElement); - Set > plugins = entry.getValue(); - for (Class> clazz : plugins) { - String className = clazz.getName(); + Set 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 >> extensionPlugins = extensionsState.getAllKnownExtensions(); + Map > extensionPlugins = extensionsState.getAllKnownExtensions(); Map xmlMementosByName = getKnownExtensions(xml); Set names = extensionPlugins.keySet(); Set newExtensions = new HashSet<>(names); @@ -115,7 +116,7 @@ class ToolExtensionsStatusManager { } } - Set > newPlugins = newExtensions.stream() + Set 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 >> pluginsByExtensionName) { + Map > 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 xmlClassNames = xmlMemento.pluginClassNames(); - Set > cpPluginClasses = pluginsByExtensionName.get(xmlMemento.name()); + Set cpPluginClasses = pluginsByExtensionName.get(xmlMemento.name()); if (xmlClassNames.isEmpty() && !cpPluginClasses.isEmpty()) { return true; } List cpNames = cpPluginClasses.stream() - .map(c -> c.getName()) + .map(c -> c.name()) .collect(Collectors.toList()); cpNames.removeAll(xmlClassNames); diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java index e8822cf2d4..95a5e03ad5 100644 --- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java +++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FrontEndPluginScreenShots.java @@ -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);