GP-3623 - Extensions - Added an extension-specific class loader; moved ExtensionUtils to Generic

This commit is contained in:
dragonmacher 2023-11-21 11:18:28 -05:00
parent 80d92aa32f
commit 0a520b08bd
30 changed files with 1079 additions and 731 deletions

View file

@ -153,7 +153,7 @@ public class PluginDescription implements Comparable<PluginDescription> {
*/
public String getModuleName() {
if (moduleName == null) {
ResourceFile moduleDir = Application.getModuleContainingClass(pluginClass.getName());
ResourceFile moduleDir = Application.getModuleContainingClass(pluginClass);
moduleName = (moduleDir == null) ? "<No Module>" : moduleDir.getName();
}

View file

@ -16,6 +16,7 @@
package ghidra.framework.plugintool.util;
import java.lang.reflect.*;
import java.util.List;
import ghidra.framework.plugintool.*;
import ghidra.util.Msg;
@ -76,6 +77,14 @@ public class PluginUtils {
*/
public static Class<? extends Plugin> forName(String pluginClassName) throws PluginException {
try {
List<Class<? extends Plugin>> classes = ClassSearcher.getClasses(Plugin.class);
for (Class<? extends Plugin> plug : classes) {
if (plug.getName().equals(pluginClassName)) {
return plug;
}
}
Class<?> tmpClass = Class.forName(pluginClassName);
if (!Plugin.class.isAssignableFrom(tmpClass)) {
throw new PluginException(
@ -84,7 +93,7 @@ public class PluginUtils {
return tmpClass.asSubclass(Plugin.class);
}
catch (ClassNotFoundException e) {
throw new PluginException("Plugin class not found");
throw new PluginException("Plugin class not found: " + pluginClassName);
}
}

View file

@ -1,376 +0,0 @@
/* ###
* 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.List;
import generic.jar.ResourceFile;
import generic.json.Json;
import ghidra.framework.Application;
import ghidra.util.Msg;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
import utility.module.ModuleUtilities;
/**
* Representation of a Ghidra extension. This class encapsulates all information required to
* uniquely identify an extension and where (or if) it has been installed.
* <p>
* Note that hashCode and equals have been implemented for this. Two extension
* descriptions are considered equal if they have the same {@link #name} attribute; all other
* fields are unimportant except for display purposes.
*/
public class ExtensionDetails implements Comparable<ExtensionDetails> {
/** Absolute path to where this extension is installed. If not installed, this will be null. */
private File installDir;
/**
* Absolute path to where the original source archive (zip) for this extension can be found. If
* there is no archive (likely because this is an extension that comes pre-installed with
* Ghidra, or Ghidra is being run in development mode), this will be null.
*/
private String archivePath;
/** Name of the extension. This must be unique.*/
private String name;
/** Brief description, for display purposes only.*/
private String description;
/** Date when the extension was created, for display purposes only.*/
private String createdOn;
/** Author of the extension, for display purposes only.*/
private String author;
/** The extension version */
private String version;
/**
* Constructor.
*
* @param name unique name of the extension; cannot be null
* @param description brief explanation of what the extension does; can be null
* @param author creator of the extension; can be null
* @param createdOn creation date of the extension, can be null
* @param version the extension version
*/
public ExtensionDetails(String name, String description, String author, String createdOn,
String version) {
this.name = name;
this.description = description;
this.author = author;
this.createdOn = createdOn;
this.version = version;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ExtensionDetails other = (ExtensionDetails) obj;
if (name == null) {
if (other.name != null) {
return false;
}
}
else if (!name.equals(other.name)) {
return false;
}
return true;
}
/**
* Returns the location where this extension is installed. If the extension is not installed
* this will be null.
*
* @return the extension path, or null
*/
public String getInstallPath() {
if (installDir != null) {
return installDir.getAbsolutePath();
}
return null;
}
public File getInstallDir() {
return installDir;
}
public void setInstallDir(File installDir) {
this.installDir = installDir;
}
/**
* Returns the location where the extension archive is located. The extension archive concept
* is not used for all extensions, but is used for delivering extensions as part of a
* distribution.
*
* @return the archive path, or null
* @see ApplicationLayout#getExtensionArchiveDir()
*/
public String getArchivePath() {
return archivePath;
}
public void setArchivePath(String path) {
this.archivePath = path;
}
public boolean isFromArchive() {
return archivePath != null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getCreatedOn() {
return createdOn;
}
public void setCreatedOn(String date) {
this.createdOn = date;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
/**
* An extension is known to be installed if it has a valid installation path AND that path
* contains a Module.manifest file. Extensions that are {@link #isPendingUninstall()} are
* still on the filesystem, may be in use by the tool, but will be removed upon restart.
* <p>
* Note: The module manifest file is a marker that indicates several things; one of which is
* the installation status of an extension. When a user marks an extension to be uninstalled (by
* checking the appropriate checkbox in the {@link ExtensionTableModel}), the only thing
* that is done is to remove this manifest file, which tells the {@link ExtensionTableProvider}
* to remove the entire extension directory on the next launch.
*
* @return true if the extension is installed.
*/
public boolean isInstalled() {
if (installDir == null) {
return false;
}
// If running out of a jar and the install path is valid, just return true. The alternative
// would be to inspect the jar and verify that the install path is there and is valid, but
// that's overkill.
if (Application.inSingleJarMode()) {
return true;
}
File f = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME);
return f.exists();
}
/**
* Returns true if this extension is marked to be uninstalled. The contents of the extension
* still exist and the tool may still be using the extension, but on restart, the extension will
* be removed.
*
* @return true if marked for uninstall
*/
public boolean isPendingUninstall() {
if (installDir == null) {
return false;
}
if (Application.inSingleJarMode()) {
return false; // can't uninstall from single jar mode
}
File f = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED);
return f.exists();
}
/**
* Returns true if this extension is installed under an installation folder or inside of a
* source control repository folder.
* @return true if this extension is installed under an installation folder or inside of a
* source control repository folder.
*/
public boolean isInstalledInInstallationFolder() {
if (installDir == null) {
return false; // not installed
}
ApplicationLayout layout = Application.getApplicationLayout();
List<ResourceFile> extDirs = layout.getExtensionInstallationDirs();
if (extDirs.size() < 2) {
Msg.trace(this, "Unexpected extension installation dirs; revisit this assumption");
return false;
}
// extDirs.get(0) is the user extension dir
ResourceFile appExtDir = extDirs.get(1);
if (FileUtilities.isPathContainedWithin(appExtDir.getFile(false), installDir)) {
return true;
}
return false;
}
/**
* Converts the module manifest and extension properties file that are in an installed state to
* an uninstalled state.
*
* Specifically, the following will be renamed:
* <UL>
* <LI>Module.manifest to Module.manifest.uninstalled</LI>
* <LI>extension.properties = extension.properties.uninstalled</LI>
* </UL>
*
* @return false if any renames fail
*/
public boolean markForUninstall() {
if (installDir == null) {
return false; // already marked as uninstalled
}
Msg.trace(this, "Marking extension for uninstall '" + installDir + "'");
boolean success = true;
File manifest = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME);
if (manifest.exists()) {
File newFile = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED);
if (!manifest.renameTo(newFile)) {
Msg.trace(this, "Unable to rename module manifest file: " + manifest);
success = false;
}
}
else {
Msg.trace(this, "No manifest file found for extension '" + name + "'");
}
File properties = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME);
if (properties.exists()) {
File newFile = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED);
if (!properties.renameTo(newFile)) {
Msg.trace(this, "Unable to rename properties file: " + properties);
success = false;
}
}
else {
Msg.trace(this, "No properties file found for extension '" + name + "'");
}
return success;
}
/**
* A companion method for {@link #markForUninstall()} that allows extensions marked for cleanup
* to be restored to the installed state.
* <p>
* Specifically, the following will be renamed:
* <UL>
* <LI>Module.manifest.uninstalled to Module.manifest</LI>
* <LI>extension.properties.uninstalled to extension.properties</LI>
* </UL>
* @return true if successful
*/
public boolean clearMarkForUninstall() {
if (installDir == null) {
Msg.error(ExtensionUtils.class,
"Cannot restore extension; extension installation dir is missing for: " + name);
return false; // already marked as uninstalled
}
Msg.trace(this, "Restoring extension state files for '" + installDir + "'");
boolean success = true;
File manifest = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED);
if (manifest.exists()) {
File newFile = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME);
if (!manifest.renameTo(newFile)) {
Msg.trace(this, "Unable to rename module manifest file: " + manifest);
success = false;
}
}
else {
Msg.trace(this, "No manifest file found for extension '" + name + "'");
}
File properties = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED);
if (properties.exists()) {
File newFile = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME);
if (!properties.renameTo(newFile)) {
Msg.trace(this, "Unable to rename properties file: " + properties);
success = false;
}
}
else {
Msg.trace(this, "No properties file found for extension '" + name + "'");
}
return success;
}
@Override
public int compareTo(ExtensionDetails other) {
return name.compareTo(other.name);
}
@Override
public String toString() {
return Json.toString(this);
}
}

View file

@ -23,6 +23,7 @@ import javax.swing.text.SimpleAttributeSet;
import docking.widgets.table.threaded.ThreadedTableModelListener;
import generic.theme.GColor;
import ghidra.framework.plugintool.dialog.AbstractDetailsPanel;
import ghidra.util.extensions.ExtensionDetails;
/**
* Panel that shows information about the selected extension in the {@link ExtensionTablePanel}. This

View file

@ -0,0 +1,276 @@
/* ###
* 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.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import docking.widgets.OkDialog;
import docking.widgets.OptionDialog;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.extensions.*;
import ghidra.util.task.TaskLauncher;
import utility.application.ApplicationLayout;
/**
* Utility class for managing Ghidra Extensions.
* <p>
* Extensions are defined as any archive or folder that contains an <code>extension.properties</code>
* file. This properties file can contain the following attributes:
* <ul>
* <li>name (required)</li>
* <li>description</li>
* <li>author</li>
* <li>createdOn (format: MM/dd/yyyy)</li>
* <li>version</li>
* </ul>
*
* <p>
* Extensions may be installed/uninstalled by users at runtime, using the
* {@link ExtensionTableProvider}. 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.
*/
public class ExtensionInstaller {
private static final Logger log = LogManager.getLogger(ExtensionInstaller.class);
/**
* Installs the given extension file. This can be either an archive (zip) or a directory that
* contains an extension.properties file.
*
* @param file the extension to install
* @return true if the extension was successfully installed
*/
public static boolean install(File file) {
log.trace("Installing extension file " + file);
if (file == null) {
log.error("Install file cannot be null");
return false;
}
ExtensionDetails extension = ExtensionUtils.getExtension(file, false);
if (extension == null) {
Msg.showError(ExtensionInstaller.class, null, "Error Installing Extension",
file.getAbsolutePath() + " does not point to a valid ghidra extension");
return false;
}
Extensions extensions = ExtensionUtils.getAllInstalledExtensions();
if (checkForConflictWithDevelopmentExtension(extension, extensions)) {
return false;
}
if (checkForDuplicateExtensions(extension, extensions)) {
return false;
}
// Verify that the version of the extension is valid for this version of Ghidra. If not,
// just exit without installing.
if (!validateExtensionVersion(extension)) {
return false;
}
AtomicBoolean installed = new AtomicBoolean(false);
TaskLauncher.launchModal("Installing Extension", (monitor) -> {
installed.set(ExtensionUtils.install(extension, file, monitor));
});
boolean success = installed.get();
if (success) {
log.trace("Finished installing " + file);
}
else {
log.trace("Failed to install " + file);
}
return success;
}
/**
* Installs the given extension from its declared archive path
* @param extension the extension
* @return true if successful
*/
public static boolean installExtensionFromArchive(ExtensionDetails extension) {
if (extension == null) {
log.error("Extension to install cannot be null");
return false;
}
String archivePath = extension.getArchivePath();
if (archivePath == null) {
log.error(
"Cannot install from archive; extension is missing archive path");
return false;
}
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile extInstallDir = layout.getExtensionInstallationDirs().get(0);
String extName = extension.getName();
File extDestinationDir = new ResourceFile(extInstallDir, extName).getFile(false);
File archiveFile = new File(archivePath);
if (install(archiveFile)) {
extension.setInstallDir(new File(extDestinationDir, extName));
return true;
}
return false;
}
/**
* Compares the given extension version to the current Ghidra version. If they are different,
* then the user will be prompted to confirm the installation. This method will return true
* if the versions match or the user has chosen to install anyway.
*
* @param extension the extension
* @return true if the versions match or the user has chosen to install anyway
*/
private static boolean validateExtensionVersion(ExtensionDetails extension) {
String extVersion = extension.getVersion();
if (extVersion == null) {
extVersion = "<no version>";
}
String appVersion = Application.getApplicationVersion();
if (extVersion.equals(appVersion)) {
return true;
}
String message = "Extension version mismatch.\nName: " + extension.getName() +
"Extension version: " + extVersion + ".\nGhidra version: " + appVersion + ".";
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
"Extension Version Mismatch",
message,
"Install Anyway");
if (choice != OptionDialog.OPTION_ONE) {
log.info(removeNewlines(message + " Did not install"));
return false;
}
return true;
}
private static String removeNewlines(String s) {
return s.replaceAll("\n", " ");
}
private static boolean checkForDuplicateExtensions(ExtensionDetails newExtension,
Extensions extensions) {
String name = newExtension.getName();
log.trace("Checking for duplicate extensions for '" + name + "'");
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
log.trace("No matching extensions installed");
return false;
}
log.trace("Duplicate extensions found by name '" + name + "'");
if (matches.size() > 1) {
reportMultipleDuplicateExtensionsWhenInstalling(newExtension, matches);
return true;
}
ExtensionDetails installedExtension = matches.get(0);
String message =
"Attempting to install an extension matching the name of an existing extension.\n" +
"New extension version: " + newExtension.getVersion() + ".\n" +
"Installed extension version: " + installedExtension.getVersion() + ".\n\n" +
"To install, click 'Remove Existing', restart Ghidra, then install again.";
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
"Duplicate Extension",
message,
"Remove Existing");
String installPath = installedExtension.getInstallPath();
if (choice != OptionDialog.OPTION_ONE) {
log.info(
removeNewlines(
message + " Skipping installation. Original extension still installed: " +
installPath));
return true;
}
//
// At this point the user would like to replace the existing extension. We cannot delete
// the existing extension, as it may be in use; mark it for removal.
//
log.info(
removeNewlines(
message + " Installing new extension. Existing extension will be removed after " +
"restart: " + installPath));
installedExtension.markForUninstall();
return true;
}
private static void reportMultipleDuplicateExtensionsWhenInstalling(ExtensionDetails extension,
List<ExtensionDetails> matches) {
StringBuilder buffy = new StringBuilder();
buffy.append("Found multiple duplicate extensions while trying to install '")
.append(extension.getName())
.append("'\n");
for (ExtensionDetails otherExtension : matches) {
buffy.append("Duplicate: " + otherExtension.getInstallPath()).append('\n');
}
buffy.append("Please close Ghidra and manually remove from these extensions from the " +
"filesystem.");
Msg.showInfo(ExtensionInstaller.class, null, "Duplicate Extensions Found",
buffy.toString());
}
private static boolean checkForConflictWithDevelopmentExtension(ExtensionDetails newExtension,
Extensions extensions) {
String name = newExtension.getName();
log.trace("Checking for duplicate dev mode extensions for '" + name + "'");
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
log.trace("No matching extensions installed");
return false;
}
for (ExtensionDetails extension : matches) {
if (extension.isInstalledInInstallationFolder()) {
String message = "Attempting to install an extension that conflicts with an " +
"extension located in the Ghidra installation folder.\nYou must manually " +
"remove the existing extension to install the new extension.\nExisting " +
"extension: " + extension.getInstallDir();
log.trace(removeNewlines(message));
OkDialog.showError("Duplicate Extensions Found", message);
return true;
}
}
return false;
}
}

View file

@ -27,6 +27,8 @@ import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.Msg;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.task.TaskMonitor;
@ -155,7 +157,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
// into this state is by clicking an extension that was discovered in the 'extension
// archives folder'
if (extension.isFromArchive()) {
if (ExtensionUtils.installExtensionFromArchive(extension)) {
if (ExtensionInstaller.installExtensionFromArchive(extension)) {
refreshTable();
}
return;
@ -192,6 +194,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
return;
}
ExtensionUtils.reload();
Set<ExtensionDetails> archived = ExtensionUtils.getArchiveExtensions();
Set<ExtensionDetails> installed = ExtensionUtils.getInstalledExtensions();

View file

@ -27,6 +27,7 @@ import docking.widgets.table.*;
import ghidra.app.util.GenericHelpTopics;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
import ghidra.util.extensions.ExtensionDetails;
import help.Help;
import help.HelpService;

View file

@ -33,6 +33,7 @@ import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
import resources.Icons;
@ -105,7 +106,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
super.dialogClosed();
if (extensionTablePanel.getTableModel().hasModelChanged() || requireRestart) {
Msg.showInfo(this, getComponent(), "Extensions Changed!",
Msg.showInfo(this, null, "Extensions Changed!",
"Please restart Ghidra for extension changes to take effect.");
}
}
@ -176,7 +177,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
continue;
}
boolean success = ExtensionUtils.install(file);
boolean success = ExtensionInstaller.install(file);
didInstall |= success;
}
return didInstall;

View file

@ -16,7 +16,6 @@
package ghidra.framework.project.tool;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
@ -29,10 +28,10 @@ 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.ExtensionDetails;
import ghidra.framework.project.extensions.ExtensionUtils;
import ghidra.util.NumericUtilities;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.xml.XmlUtilities;
import utilities.util.FileUtilities;
@ -191,12 +190,7 @@ class ExtensionManager {
Set<PluginPath> pluginPaths = getPluginPaths();
Set<Class<?>> extensionPlugins = new HashSet<>();
for (ExtensionDetails extension : extensions) {
File installDir = extension.getInstallDir();
if (installDir == null) {
continue;
}
Set<Class<?>> classes = findPluginsLoadedFromExtension(installDir, pluginPaths);
Set<Class<?>> classes = findPluginsLoadedFromExtension(extension, pluginPaths);
extensionPlugins.addAll(classes);
}
@ -219,28 +213,31 @@ class ExtensionManager {
* classpath. For each class, the original resource file is compared against the
* given extension folder and the jar files for that extension.
*
* @param dir the directory to search, or a jar file
* @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(File dir,
private static Set<Class<?>> findPluginsLoadedFromExtension(ExtensionDetails extension,
Set<PluginPath> pluginPaths) {
Set<Class<?>> result = new HashSet<>();
if (!extension.isInstalled()) {
return Collections.emptySet();
}
// Find any jar files in the directory provided
Set<String> jarPaths = getJarPaths(dir);
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(dir)) {
if (pluginPath.isFrom(extension.getInstallDir())) {
result.add(pluginPath.getPluginClass());
continue;
}
for (String jarPath : jarPaths) {
if (pluginPath.isFrom(jarPath)) {
for (URL jarUrl : jarPaths) {
if (pluginPath.isFrom(jarUrl)) {
result.add(pluginPath.getPluginClass());
}
}
@ -248,45 +245,6 @@ class ExtensionManager {
return result;
}
private static Set<String> getJarPaths(File dir) {
Set<File> jarFiles = new HashSet<>();
findJarFiles(dir, jarFiles);
Set<String> paths = new HashSet<>();
for (File jar : jarFiles) {
try {
URL jarUrl = jar.toURI().toURL();
paths.add(jarUrl.getPath());
}
catch (MalformedURLException e) {
continue;
}
}
return paths;
}
/**
* Populates the given list with all discovered jar files found in the given directory and
* its subdirectories.
*
* @param dir the directory to search
* @param jarFiles list of found jar files
*/
private static void findJarFiles(File dir, Set<File> jarFiles) {
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
findJarFiles(f, jarFiles);
}
if (f.isFile() && f.getName().endsWith(".jar")) {
jarFiles.add(f);
}
}
}
private static class PluginPath {
private Class<? extends Plugin> pluginClass;
private String pluginLocation;
@ -304,7 +262,8 @@ class ExtensionManager {
return FileUtilities.isPathContainedWithin(dir, pluginFile);
}
boolean isFrom(String jarPath) {
boolean isFrom(URL jarUrl) {
String jarPath = jarUrl.getPath();
return pluginLocation.contains(jarPath);
}

View file

@ -31,15 +31,17 @@ import docking.DialogComponentProvider;
import docking.test.AbstractDockingTest;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
import utility.function.ExceptionalCallback;
import utility.module.ModuleUtilities;
/**
* Tests for the {@link ExtensionUtils} class.
* Tests for the {@link ExtensionInstaller} class.
*/
public class ExtensionUtilsTest extends AbstractDockingTest {
public class ExtensionInstallerTest extends AbstractDockingTest {
private static final String BUILD_FOLDER_NAME = "TestExtensionParentDir";
private static final String TEST_EXT_NAME = "test";
@ -87,7 +89,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
// Create an extension and install it.
File file = createExtensionZip(TEST_EXT_NAME);
ExtensionUtils.install(file);
ExtensionInstaller.install(file);
// Verify there is something in the installation directory and it has the correct name
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
@ -101,7 +103,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
// Create an extension and install it.
File file = createExtensionFolderInArchiveDir();
ExtensionUtils.install(file);
ExtensionInstaller.install(file);
// Verify the extension is in the install folder and has the correct name
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
@ -142,10 +144,9 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
@Test
public void testBadInputs() throws Exception {
errorsExpected(() -> {
assertFalse(ExtensionUtils.isExtension(null));
assertFalse(ExtensionUtils.install(new File("this/file/does/not/exist")));
assertFalse(ExtensionUtils.install(null));
assertFalse(ExtensionUtils.installExtensionFromArchive(null));
assertFalse(ExtensionInstaller.install(new File("this/file/does/not/exist")));
assertFalse(ExtensionInstaller.install(null));
assertFalse(ExtensionInstaller.installExtensionFromArchive(null));
});
}
@ -156,7 +157,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
extension.setArchivePath(zipFile.getAbsolutePath());
String ghidraVersion = Application.getApplicationVersion();
extension.setVersion(ghidraVersion);
assertTrue(ExtensionUtils.installExtensionFromArchive(extension));
assertTrue(ExtensionInstaller.installExtensionFromArchive(extension));
}
@Test
@ -168,7 +169,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
didInstall.set(ExtensionInstaller.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
@ -187,7 +188,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
didInstall.set(ExtensionInstaller.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
@ -207,7 +208,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
didInstall.set(ExtensionInstaller.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
@ -221,7 +222,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testMarkForUninstall_ClearMark() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
@ -238,7 +239,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testCleanupUninstalledExtions_WithExtensionMarkedForUninstall() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
@ -255,8 +256,8 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testCleanupUninstalledExtions_SomeExtensionMarkedForUninstall() throws Exception {
List<File> extensionFolders = createTwoExternalExtensionsInFolder();
assertTrue(ExtensionUtils.install(extensionFolders.get(0)));
assertTrue(ExtensionUtils.install(extensionFolders.get(1)));
assertTrue(ExtensionInstaller.install(extensionFolders.get(0)));
assertTrue(ExtensionInstaller.install(extensionFolders.get(1)));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 2);
@ -279,7 +280,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testCleanupUninstalledExtions_NoExtensionsMarkedForUninstall() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
assertExtensionInstalled(TEST_EXT_NAME);
// This should not uninstall any extensions
@ -299,7 +300,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
assertTrue(ExtensionInstaller.install(extensionFolder));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 1);
@ -313,7 +314,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
@ -329,7 +330,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
checkCleanInstall();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
// no longer an installed extension conflict; now we have a version mismatch
@ -349,7 +350,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
assertTrue(ExtensionInstaller.install(extensionFolder));
// create another extension Foo v2
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
@ -359,7 +360,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
@ -379,7 +380,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
assertTrue(ExtensionInstaller.install(extensionFolder));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 1);
@ -393,7 +394,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
@ -409,7 +410,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
checkCleanInstall();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
waitFor(didInstall);
@ -437,7 +438,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
errorsExpected(() -> {
File zipFile = createZipWithMultipleExtensions();
assertFalse(ExtensionUtils.install(zipFile));
assertFalse(ExtensionInstaller.install(zipFile));
});
}
@ -452,7 +453,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String nameProperty = "ExtensionNamedFoo";
File externalFolder = createExtensionWithMismatchingNamePropertyString(nameProperty);
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(nameProperty);