GP-3569 - Cleanup of Extension management

This commit is contained in:
dragonmacher 2023-07-11 14:09:56 -04:00
parent b0e0c7372a
commit b7583dc0b9
61 changed files with 3058 additions and 2540 deletions

View file

@ -16,7 +16,7 @@
package ghidra.framework.main;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.util.PluginsConfiguration;
import ghidra.framework.plugintool.PluginsConfiguration;
/**
* A configuration that only includes {@link ApplicationLevelPlugin} plugins.

View file

@ -647,7 +647,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
}
};
MenuData menuData =
new MenuData(new String[] { ToolConstants.MENU_FILE, "Install Extensions..." }, null,
new MenuData(new String[] { ToolConstants.MENU_FILE, "Install Extensions" }, null,
CONFIGURE_GROUP);
menuData.setMenuSubGroup(CONFIGURE_GROUP + 2);
installExtensionsAction.setMenuBarData(menuData);
@ -674,7 +674,7 @@ public class FrontEndTool extends PluginTool implements OptionsChangeListener {
}
};
MenuData menuData = new MenuData(new String[] { ToolConstants.MENU_FILE, "Configure..." },
MenuData menuData = new MenuData(new String[] { ToolConstants.MENU_FILE, "Configure" },
null, CONFIGURE_GROUP);
menuData.setMenuSubGroup(CONFIGURE_GROUP + 1);
configureToolAction.setMenuBarData(menuData);

View file

@ -20,7 +20,6 @@ import java.net.URL;
import java.util.Collection;
import java.util.Set;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.protocol.ghidra.GhidraURL;
@ -63,18 +62,6 @@ public interface ToolServices {
*/
public ToolChest getToolChest();
/**
* Find a running tool like the one specified that has the named domain file.
* If it finds a matching tool, then it is brought to the front.
* Otherwise, it creates one and runs it.
* It then invokes the specified event on the running tool.
*
* @param tool find/create a tool like this one.
* @param domainFile open this file in the found/created tool.
* @param event invoke this event on the found/created tool
*/
public void displaySimilarTool(PluginTool tool, DomainFile domainFile, PluginEvent event);
/**
* Returns the default/preferred tool template which should be used to open the specified
* domain file, whether defined by the user or the system default.

View file

@ -17,7 +17,6 @@ package ghidra.framework.plugintool;
import ghidra.framework.main.AppInfo;
import ghidra.framework.model.Project;
import ghidra.framework.plugintool.util.PluginsConfiguration;
/**
* PluginTool that is used by the Merge process to resolve conflicts

View file

@ -50,11 +50,11 @@ import ghidra.framework.main.AppInfo;
import ghidra.framework.main.UserAgreementDialog;
import ghidra.framework.model.*;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.dialog.ExtensionTableProvider;
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.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
@ -198,7 +198,7 @@ public abstract class PluginTool extends AbstractDockingTool {
return new DefaultPluginsConfiguration();
}
protected PluginsConfiguration getPluginsConfiguration() {
public PluginsConfiguration getPluginsConfiguration() {
return pluginMgr.getPluginsConfiguration();
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.plugintool.util;
package ghidra.framework.plugintool;
import static java.util.function.Predicate.*;
@ -23,7 +23,7 @@ import java.util.function.Predicate;
import org.jdom.Element;
import ghidra.framework.main.ProgramaticUseOnly;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.util.*;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
@ -56,7 +56,7 @@ public abstract class PluginsConfiguration {
List<Class<? extends Plugin>> classes = ClassSearcher.getClasses(Plugin.class, classFilter);
for (Class<? extends Plugin> pluginClass : classes) {
if (!PluginUtils.isValidPluginClass(pluginClass)) {
if (!isValidPluginClass(pluginClass)) {
Msg.warn(this, "Plugin does not have valid constructor! Skipping " + pluginClass);
continue;
}
@ -72,6 +72,19 @@ public abstract class PluginsConfiguration {
}
private boolean isValidPluginClass(Class<? extends Plugin> pluginClass) {
try {
// will throw exception if missing constructor
pluginClass.getConstructor(PluginTool.class);
return true;
}
catch (NoSuchMethodException e) {
// no matching constructor method
}
return false;
}
public PluginDescription getPluginDescription(String className) {
return descriptionsByName.get(className);
}

View file

@ -23,7 +23,6 @@ import docking.action.*;
import docking.tool.ToolConstants;
import ghidra.framework.OperatingSystem;
import ghidra.framework.Platform;
import ghidra.framework.plugintool.util.PluginsConfiguration;
import ghidra.util.HelpLocation;
public class StandAlonePluginTool extends PluginTool {

View file

@ -34,11 +34,6 @@ public class ToolServicesAdapter implements ToolServices {
// override
}
@Override
public void displaySimilarTool(PluginTool tool, DomainFile domainFile, PluginEvent event) {
// override
}
@Override
public File exportTool(ToolTemplate tool) throws FileNotFoundException, IOException {
return null;

View file

@ -28,9 +28,7 @@ import generic.theme.GColor;
import ghidra.util.HTMLUtilities;
/**
* Abstract class that defines a panel for displaying name/value pairs with html-formatting.
* <p>
* This is used with the {@link ExtensionDetailsPanel} and the {@link PluginDetailsPanel}
* Abstract class that defines a panel for displaying name/value pairs with html-formatting.
*/
public abstract class AbstractDetailsPanel extends JPanel {

View file

@ -1,208 +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.plugintool.dialog;
import java.io.File;
import ghidra.framework.Application;
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 save 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 String installPath;
/**
* 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() {
return installPath;
}
public void setInstallPath(String path) {
this.installPath = path;
}
/**
* Returns the location where the extension archive is located. If there is no
* archive this will be null.
*
* @return the archive path, or null
*/
public String getArchivePath() {
return archivePath;
}
public void setArchivePath(String path) {
this.archivePath = path;
}
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.
* <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 (installPath == null || installPath.isEmpty()) {
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 mm = new File(installPath, ModuleUtilities.MANIFEST_FILE_NAME);
return mm.exists();
}
@Override
public int compareTo(ExtensionDetails other) {
return name.compareTo(other.name);
}
}

View file

@ -1,70 +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.plugintool.dialog;
import java.io.File;
import ghidra.util.exception.UsrException;
/**
* Defines an exception that can be thrown by {@link ExtensionUtils}. This is intended to provide
* detailed information about issues that arise during installation (or removal) of
* Extensions.
*
*/
public class ExtensionException extends UsrException {
/** Provides more detail as to the specific source of the exception. */
public enum ExtensionExceptionType {
/** Thrown if the required installation location does not exist */
INVALID_INSTALL_LOCATION,
/** Thrown when installing an extension to an existing location */
DUPLICATE_FILE_ERROR,
/** Thrown when there is a problem reading/extracting a zip file during installation */
ZIP_ERROR,
/** Thrown when there is a problem copying a folder during an installation */
COPY_ERROR,
/** Thrown when the user cancels the installation */
INSTALL_CANCELLED
}
private ExtensionExceptionType exceptionType;
private File errorFile = null; // If there's a file relevant to the exception, populate this.
public ExtensionException(String msg, ExtensionExceptionType exceptionType) {
super(msg);
this.exceptionType = exceptionType;
}
public ExtensionException(String msg, ExtensionExceptionType exceptionType, File errorFile) {
super(msg);
this.errorFile = errorFile;
this.exceptionType = exceptionType;
}
public ExtensionExceptionType getExceptionType() {
return exceptionType;
}
public File getErrorFile() {
return errorFile;
}
}

View file

@ -15,6 +15,7 @@
*/
package ghidra.framework.plugintool.dialog;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
@ -93,6 +94,8 @@ class PluginInstallerTableModel
descriptor.addVisibleColumn(new PluginNameColumn(), 1, true);
descriptor.addVisibleColumn(new PluginDescriptionColumn());
descriptor.addVisibleColumn(new PluginCategoryColumn());
descriptor.addHiddenColumn(new PluginModuleColumn());
descriptor.addHiddenColumn(new PluginLocationColumn());
return descriptor;
}
@ -177,7 +180,7 @@ class PluginInstallerTableModel
* Column for displaying the interactive checkbox, allowing the user to install
* or uninstall the plugin.
*/
class PluginInstalledColumn extends
private class PluginInstalledColumn extends
AbstractDynamicTableColumn<PluginDescription, Boolean, List<PluginDescription>> {
@Override
@ -200,7 +203,7 @@ class PluginInstallerTableModel
/**
* Column for displaying the status of the plugin.
*/
class PluginStatusColumn
private class PluginStatusColumn
extends AbstractDynamicTableColumn<PluginDescription, Icon, List<PluginDescription>> {
@Override
@ -223,7 +226,7 @@ class PluginInstallerTableModel
/**
* Column for displaying the extension name of the plugin.
*/
class PluginNameColumn
private class PluginNameColumn
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
@Override
@ -246,7 +249,7 @@ class PluginInstallerTableModel
/**
* Column for displaying the plugin description.
*/
class PluginDescriptionColumn
private class PluginDescriptionColumn
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
@Override
@ -266,10 +269,54 @@ class PluginInstallerTableModel
}
}
private class PluginModuleColumn
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
@Override
public String getColumnName() {
return "Module";
}
@Override
public int getColumnPreferredWidth() {
return 200;
}
@Override
public String getValue(PluginDescription rowObject, Settings settings,
List<PluginDescription> data, ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getModuleName();
}
}
private class PluginLocationColumn
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
@Override
public String getColumnName() {
return "Location";
}
@Override
public int getColumnPreferredWidth() {
return 200;
}
@Override
public String getValue(PluginDescription rowObject, Settings settings,
List<PluginDescription> data, ServiceProvider sp) throws IllegalArgumentException {
Class<? extends Plugin> clazz = rowObject.getPluginClass();
String name = clazz.getName();
String path = '/' + name.replace('.', '/') + ".class";
URL url = clazz.getResource(path);
return url.getFile();
}
}
/**
* Column for displaying the plugin category.
*/
class PluginCategoryColumn
private class PluginCategoryColumn
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
@Override

View file

@ -16,6 +16,7 @@
package ghidra.framework.plugintool.util;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.PluginsConfiguration;
/**
* A configuration that includes all plugins on the classpath.

View file

@ -148,14 +148,13 @@ public class PluginDescription implements Comparable<PluginDescription> {
}
/**
* Return the type for the plugin: CORE, CONTRIB, PROTOTYPE, or
* DEVELOP. Within a type, plugins are grouped by category.
* @return the type (or null if there is no module)
* Return the name of the module that contains the plugin.
* @return the module name
*/
public String getModuleName() {
if (moduleName == null) {
ResourceFile moduleRootDirectory = Application.getMyModuleRootDirectory();
moduleName = (moduleRootDirectory == null) ? null : moduleRootDirectory.getName();
ResourceFile moduleDir = Application.getModuleContainingClass(pluginClass.getName());
moduleName = (moduleDir == null) ? "<No Module>" : moduleDir.getName();
}
return moduleName;

View file

@ -15,14 +15,9 @@
*/
package ghidra.framework.plugintool.util;
import java.io.File;
import java.lang.reflect.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.dialog.ExtensionDetails;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.AssertException;
@ -33,99 +28,6 @@ import ghidra.util.exception.AssertException;
*/
public class PluginUtils {
/**
* Finds all plugin classes loaded from a given set of extensions.
*
* @param extensions set of extensions to search
* @return list of loaded plugin classes, or empty list if none found
*/
public static List<Class<?>> findLoadedPlugins(Set<ExtensionDetails> extensions) {
List<Class<?>> pluginClasses = new ArrayList<>();
for (ExtensionDetails extension : extensions) {
if (extension == null || extension.getInstallPath() == null) {
continue;
}
List<Class<?>> classes = findLoadedPlugins(new File(extension.getInstallPath()));
pluginClasses.addAll(classes);
}
return pluginClasses;
}
/**
* Finds all plugin classes loaded from a particular folder/file.
* <p>
* This uses the {@link ClassSearcher} to find all <code>Plugin.class</code> objects on the
* classpath. For each class, the original resource file is compared against the
* given folder and if it's contained therein (or if it matches a given jar), it's
* added to the return list.
*
* @param dir the directory to search, or a jar file
* @return list of {@link Plugin} classes, or empty list if none found
*/
private static List<Class<?>> findLoadedPlugins(File dir) {
// The list of classes to return.
List<Class<?>> retPlugins = new ArrayList<>();
// Find any jar files in the directory provided. Our plugin(s) will always be
// in a jar.
List<File> jarFiles = new ArrayList<>();
findJarFiles(dir, jarFiles);
// Now get all Plugin.class files that have been loaded, and see if any of them
// were loaded from one of the jars we just found.
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
for (Class<? extends Plugin> plugin : plugins) {
URL location = plugin.getResource('/' + plugin.getName().replace('.', '/') + ".class");
if (location == null) {
Msg.warn(null, "Class location for plugin [" + plugin.getName() +
"] could not be determined.");
continue;
}
String pluginLocation = location.getPath();
for (File jar : jarFiles) {
URL jarUrl = null;
try {
jarUrl = jar.toURI().toURL();
if (pluginLocation.contains(jarUrl.getPath())) {
retPlugins.add(plugin);
}
}
catch (MalformedURLException e) {
continue;
}
}
}
return retPlugins;
}
/**
* 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, List<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);
}
}
}
/**
* Returns a new instance of a {@link Plugin}.
*
@ -268,37 +170,4 @@ public class PluginUtils {
}
}
}
/**
* Returns true if the specified Plugin class is well-formed and meets requirements for
* Ghidra Plugins:
* <ul>
* <li>Has a constructor with a signature of <code>ThePlugin(PluginTool tool)</code>
* <li>Has a {@link PluginInfo @PluginInfo} annotation.
* </ul>
* <p>
* See {@link Plugin}.
* <p>
* @param pluginClass Class to examine.
* @return boolean true if well formed.
*/
public static boolean isValidPluginClass(Class<? extends Plugin> pluginClass) {
try {
// will throw exception if missing ctor
pluginClass.getConstructor(PluginTool.class);
// #if ( can_do_strict_checking )
// PluginInfo pia = pluginClass.getAnnotation(PluginInfo.class);
// return pia != null;
// #else
// for now
return true;
// #endif
}
catch (NoSuchMethodException e) {
// no matching constructor method
}
return false;
}
}

View file

@ -0,0 +1,366 @@
/* ###
* 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 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();
File appInstallDir = layout.getApplicationInstallationDir().getFile(false);
if (FileUtilities.isPathContainedWithin(appInstallDir, 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

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.plugintool.dialog;
package ghidra.framework.project.extensions;
import java.awt.Color;
import java.awt.Point;
@ -22,6 +22,7 @@ import javax.swing.text.SimpleAttributeSet;
import docking.widgets.table.threaded.ThreadedTableModelListener;
import generic.theme.GColor;
import ghidra.framework.plugintool.dialog.AbstractDetailsPanel;
/**
* Panel that shows information about the selected extension in the {@link ExtensionTablePanel}. This

View file

@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.plugintool.dialog;
package ghidra.framework.project.extensions;
import java.awt.Component;
import java.io.File;
import java.util.*;
import docking.widgets.table.*;
@ -26,13 +25,11 @@ import ghidra.docking.settings.Settings;
import ghidra.framework.Application;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
/**
* Model for the {@link ExtensionTablePanel}. This defines 5 columns for displaying information in
@ -59,9 +56,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
/** This is the data source for the model. Whatever is here will be displayed in the table. */
private Set<ExtensionDetails> extensions;
/** Indicates if the model has changed due to an install or uninstall. */
private boolean modelChanged = false;
private Map<String, Boolean> originalInstallStates = new HashMap<>();
/**
* Constructor.
@ -94,21 +89,17 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
if (Application.inSingleJarMode() || SystemUtilities.isInDevelopmentMode()) {
if (Application.inSingleJarMode()) {
return false;
}
// Do not allow GUI removal of extensions manually installed in installation directory.
ExtensionDetails extension = getSelectedExtension(rowIndex);
// Do not allow GUI uninstallation of extensions manually installed in installation
// directory
if (extension.getInstallPath() != null && FileUtilities.isPathContainedWithin(
Application.getApplicationLayout().getApplicationInstallationDir().getFile(false),
new File(extension.getInstallPath()))) {
if (extension.isInstalledInInstallationFolder()) {
return false;
}
return (columnIndex == INSTALLED_COL);
return columnIndex == INSTALLED_COL;
}
/**
@ -117,7 +108,6 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
*/
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
super.setValueAt(aValue, rowIndex, columnIndex);
// We only care about the install column here, as it's the only one that
// is editable.
@ -131,31 +121,50 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
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.\n" +
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
"Cannot install/uninstall extensions: Failed to create extension installation " +
"directory.\nSee the \"Ghidra Extension Notes\" section of the Ghidra " +
"Installation Guide for more information.");
}
if (!installDir.canWrite()) {
Msg.showError(this, null, "Permissions Error",
"Cannot install/uninstall extensions: Invalid write permissions on installation directory.\n" +
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
"Cannot install/uninstall extensions: Invalid write permissions on installation " +
"directory.\nSee the \"Ghidra Extension Notes\" section of the Ghidra " +
"Installation Guide for more information.");
return;
}
boolean install = ((Boolean) aValue).booleanValue();
ExtensionDetails extension = getSelectedExtension(rowIndex);
if (install) {
if (ExtensionUtils.install(extension, true)) {
modelChanged = true;
if (!install) {
if (extension.markForUninstall()) {
refreshTable();
}
return;
}
else {
if (ExtensionUtils.removeStateFiles(extension)) {
modelChanged = true;
// Restore an existing extension or install an archived extension
if (extension.isPendingUninstall()) {
if (extension.clearMarkForUninstall()) {
refreshTable();
return;
}
}
refreshTable();
// At this point, the extension is not installed, so we cannot simply clear the uninstall
// state. This means that the extension has not yet been installed. The only way to get
// into this state is by clicking an extension that was discovered in the 'extension
// archives folder'
if (extension.isFromArchive()) {
if (ExtensionUtils.installExtensionFromArchive(extension)) {
refreshTable();
}
return;
}
// 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() + "'");
}
/**
@ -164,10 +173,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* @param details the extension to check
* @return true if extension version is valid for this version of Ghidra
*/
private boolean isValidVersion(ExtensionDetails details) {
private boolean matchesGhidraVersion(ExtensionDetails details) {
String ghidraVersion = Application.getApplicationVersion();
String extensionVersion = details.getVersion();
return ghidraVersion.equals(extensionVersion);
}
@ -184,12 +192,28 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
return;
}
try {
extensions = ExtensionUtils.getExtensions();
Set<ExtensionDetails> archived = ExtensionUtils.getArchiveExtensions();
Set<ExtensionDetails> installed = ExtensionUtils.getInstalledExtensions();
// don't show archived extensions that have been installed
for (ExtensionDetails extension : installed) {
if (archived.remove(extension)) {
Msg.trace(this,
"Not showing archived extension that has been installed. Archive path: " +
extension.getArchivePath()); // useful for debugging
}
}
catch (ExtensionException e) {
Msg.error(this, "Error loading extensions", e);
return;
extensions = new HashSet<>();
extensions.addAll(installed);
extensions.addAll(archived);
for (ExtensionDetails e : extensions) {
String name = e.getName();
if (originalInstallStates.containsKey(name)) {
continue; // preserve the original value
}
originalInstallStates.put(e.getName(), e.isInstalled());
}
accumulator.addAll(extensions);
@ -201,7 +225,15 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
* @return true if the model has changed as a result of installing or uninstalling an extension
*/
public boolean hasModelChanged() {
return modelChanged;
for (ExtensionDetails e : extensions) {
Boolean wasInstalled = originalInstallStates.get(e.getName());
if (e.isInstalled() != wasInstalled) {
return true;
}
}
return false;
}
/**
@ -241,7 +273,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
private class ExtensionNameColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
private ExtVersionRenderer renderer = new ExtVersionRenderer();
private ExtRenderer renderer = new ExtRenderer();
@Override
public String getColumnName() {
@ -271,7 +303,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
private class ExtensionDescriptionColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
private ExtVersionRenderer renderer = new ExtVersionRenderer();
private ExtRenderer renderer = new ExtRenderer();
@Override
public String getColumnName() {
@ -301,7 +333,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
private class ExtensionVersionColumn
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
private ExtVersionRenderer renderer = new ExtVersionRenderer();
private ExtRenderer renderer = new ExtRenderer();
@Override
public String getColumnName() {
@ -403,14 +435,14 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
}
}
private class ExtVersionRenderer extends AbstractGColumnRenderer<String> {
private class ExtRenderer extends AbstractGColumnRenderer<String> {
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
Component comp = super.getTableCellRendererComponent(data);
ExtensionDetails extension = getSelectedExtension(data.getRowViewIndex());
if (!isValidVersion(extension)) {
if (!matchesGhidraVersion(extension)) {
comp.setForeground(getErrorForegroundColor(data.isSelected()));
}

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.plugintool.dialog;
package ghidra.framework.project.extensions;
import java.awt.BorderLayout;
import java.awt.Dimension;
@ -70,9 +70,6 @@ public class ExtensionTablePanel extends JPanel {
// way to restrict column width.
TableColumn col = table.getColumnModel().getColumn(ExtensionTableModel.INSTALLED_COL);
col.setMaxWidth(25);
// Finally, load the table with some data.
refreshTable();
}
public void dispose() {

View file

@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.plugintool.dialog;
package ghidra.framework.project.extensions;
import java.awt.BorderLayout;
import java.io.File;
import java.util.List;
import java.util.Properties;
import javax.swing.*;
@ -32,7 +31,8 @@ import generic.jar.ResourceFile;
import ghidra.app.util.GenericHelpTopics;
import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.*;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
import resources.Icons;
@ -43,6 +43,8 @@ import resources.Icons;
*/
public class ExtensionTableProvider extends DialogComponentProvider {
private static final String LAST_IMPORT_DIRECTORY_KEY = "LastExtensionImportDirectory";
private ExtensionTablePanel extensionTablePanel;
private boolean requireRestart = false;
@ -126,57 +128,28 @@ public class ExtensionTableProvider extends DialogComponentProvider {
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.\n" +
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
"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.\n" +
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
"Cannot install/uninstall extensions: Invalid write permissions on " +
"installation directory: " + installDir);
return;
}
GhidraFileChooser chooser = new GhidraFileChooser(getComponent());
chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_AND_DIRECTORIES);
chooser.setTitle("Select extension");
chooser.setLastDirectoryPreference(LAST_IMPORT_DIRECTORY_KEY);
chooser.setTitle("Select Extension");
chooser.addFileFilter(new ExtensionFileFilter());
List<File> files = chooser.getSelectedFiles();
chooser.dispose();
for (File file : files) {
try {
if (!ExtensionUtils.isExtension(new ResourceFile(file))) {
Msg.showError(this, null, "Installation Error", "Selected file: [" +
file.getName() + "] is not a valid Ghidra Extension");
continue;
}
}
catch (ExtensionException e1) {
Msg.showError(this, null, "Installation Error", "Error determining if [" +
file.getName() + "] is a valid Ghidra Extension", e1);
continue;
}
String extensionVersion = getExtensionVersion(file);
if (extensionVersion == null) {
Msg.showError(this, null, "Installation Error",
"Unable to read extension version for [" + file + "]");
continue;
}
if (!ExtensionUtils.validateExtensionVersion(extensionVersion)) {
continue;
}
try {
if (ExtensionUtils.install(new ResourceFile(file))) {
panel.refreshTable();
requireRestart = true;
}
}
catch (Exception e) {
Msg.error(null, "Problem installing extension [" + file.getName() + "]", e);
}
if (installExtensions(files)) {
panel.refreshTable();
requireRestart = true;
}
}
};
@ -185,49 +158,18 @@ public class ExtensionTableProvider extends DialogComponentProvider {
addAction.setMenuBarData(new MenuData(new String[] { "Add Extension" }, addIcon, group));
addAction.setToolBarData(new ToolBarData(addIcon, group));
addAction.setHelpLocation(new HelpLocation(GenericHelpTopics.FRONT_END, "ExtensionTools"));
addAction.setDescription(
SystemUtilities.isInDevelopmentMode() ? "Add Extension (disabled in development mode)"
: "Add extension");
addAction.setEnabled(
!SystemUtilities.isInDevelopmentMode() && !Application.inSingleJarMode());
addAction.setDescription("Add extension");
addAction.setEnabled(!Application.inSingleJarMode());
addAction(addAction);
}
private String getExtensionVersion(File file) {
// If the given file is a directory...
if (!file.isFile()) {
List<ResourceFile> propFiles =
ExtensionUtils.findExtensionPropertyFiles(new ResourceFile(file), true);
for (ResourceFile props : propFiles) {
ExtensionDetails ext = ExtensionUtils.createExtensionDetailsFromPropertyFile(props);
String version = ext.getVersion();
if (version != null) {
return version;
}
}
return null;
private boolean installExtensions(List<File> files) {
boolean didInstall = false;
for (File file : files) {
boolean success = ExtensionUtils.install(file);
didInstall |= success;
}
// If the given file is a zip...
try {
if (ExtensionUtils.isZip(file)) {
Properties props = ExtensionUtils.getPropertiesFromArchive(file);
if (props == null) {
return null; // no prop file exists
}
ExtensionDetails ext = ExtensionUtils.createExtensionDetailsFromProperties(props);
String version = ext.getVersion();
if (version != null) {
return version;
}
}
}
catch (ExtensionException e) {
// just fall through
}
return null;
return didInstall;
}
/**
@ -258,9 +200,8 @@ public class ExtensionTableProvider extends DialogComponentProvider {
}
/**
* Filter for a {@link GhidraFileChooser} that restricts selection to those
* files that are Ghidra Extensions (zip files with an extension.properties
* file) or folders.
* Filter for a {@link GhidraFileChooser} that restricts selection to those files that are
* Ghidra Extensions (zip files with an extension.properties file) or folders.
*/
private class ExtensionFileFilter implements GhidraFileFilter {
@Override
@ -269,16 +210,8 @@ public class ExtensionTableProvider extends DialogComponentProvider {
}
@Override
public boolean accept(File f, GhidraFileChooserModel l_model) {
try {
return ExtensionUtils.isExtension(new ResourceFile(f)) || f.isDirectory();
}
catch (ExtensionException e) {
// if something fails to be recognized as an extension, just move on.
}
return false;
public boolean accept(File f, GhidraFileChooserModel model) {
return f.isDirectory() || ExtensionUtils.isExtension(f);
}
}
}

View file

@ -0,0 +1,953 @@
/* ###
* 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.*;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.utils.IOUtils;
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.exception.CancelledException;
import ghidra.util.task.TaskLauncher;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
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 ExtensionUtils {
/** Magic number that identifies the first bytes of a ZIP archive. This is used to verify
that a file is a zip rather than just checking the extension. */
private static final int ZIPFILE = 0x504b0304;
public static String PROPERTIES_FILE_NAME = "extension.properties";
public static String PROPERTIES_FILE_NAME_UNINSTALLED = "extension.properties.uninstalled";
private static final Logger log = LogManager.getLogger(ExtensionUtils.class);
/**
* Performs extension maintenance. This should be called at startup, before any plugins or
* extension points are loaded.
*/
public static void initializeExtensions() {
Extensions extensions = getAllInstalledExtensions();
// delete any extensions marked for removal
extensions.cleanupExtensionsMarkedForRemoval();
// check for duplicates in the remaining extensions
extensions.reportDuplicateExtensions();
}
/**
* Gets all known extensions that have not been marked for removal.
*
* @return set of installed extensions
*/
public static Set<ExtensionDetails> getActiveInstalledExtensions() {
Extensions extensions = getAllInstalledExtensions();
return extensions.getActiveExtensions();
}
/**
* Returns all installed extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionInstallationDirs}.
*
* @return set of installed extensions
*/
public static Set<ExtensionDetails> getInstalledExtensions() {
Extensions extensions = getAllInstalledExtensions();
return extensions.get();
}
private static Extensions getAllInstalledExtensions() {
Extensions extensions = new Extensions();
// Find all extension.properties or extension.properties.uninstalled files in
// the install directory and create a ExtensionDetails object for each.
ApplicationLayout layout = Application.getApplicationLayout();
for (ResourceFile installDir : layout.getExtensionInstallationDirs()) {
if (!installDir.isDirectory()) {
continue;
}
log.trace("Checking extension installation dir '" + installDir);
File dir = installDir.getFile(false);
List<File> propFiles = findExtensionPropertyFiles(dir);
for (File propFile : propFiles) {
ExtensionDetails extension = createExtensionFromProperties(propFile);
if (extension == null) {
continue;
}
// We found this extension in the installation directory, so set the install path
// property and add to the final set.
File extInstallDir = propFile.getParentFile();
extension.setInstallDir(extInstallDir);
log.trace("Loading extension '" + extension.getName() + "' from: " + extInstallDir);
extensions.add(extension);
}
}
return extensions;
}
/**
* Returns all archive extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionArchiveDir}. This are added to an installation as
* part of the build processes.
* <p>
* Archived extensions may be zip files and directories.
*
* @return set of archive extensions
*/
public static Set<ExtensionDetails> getArchiveExtensions() {
log.trace("Finding archived extensions");
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile archiveDir = layout.getExtensionArchiveDir();
if (archiveDir == null) {
log.trace("No extension archive dir found");
return Collections.emptySet();
}
ResourceFile[] archiveFiles = archiveDir.listFiles();
if (archiveFiles == null) {
log.trace("No files in extension archive dir: " + archiveDir);
return Collections.emptySet(); // no files or dirs inside of the archive directory
}
Set<ExtensionDetails> extensions = new HashSet<>();
findExtensionsInZips(archiveFiles, extensions);
findExtensionsInFolder(archiveDir.getFile(false), extensions);
return extensions;
}
private static void findExtensionsInZips(ResourceFile[] archiveFiles,
Set<ExtensionDetails> extensions) {
for (ResourceFile file : archiveFiles) {
ExtensionDetails extension = createExtensionDetailsFromArchive(file);
if (extension == null) {
log.trace("Skipping archive file; not an extension: " + file);
continue;
}
if (extensions.contains(extension)) {
log.error(
"Skipping extension \"" + extension.getName() + "\" found at " +
extension.getInstallPath() +
".\nArchived extension by that name already found.");
}
extensions.add(extension);
}
}
private static void findExtensionsInFolder(File dir, Set<ExtensionDetails> extensions) {
List<File> propFiles = findExtensionPropertyFiles(dir);
for (File propFile : propFiles) {
ExtensionDetails extension = createExtensionFromProperties(propFile);
if (extension == null) {
continue;
}
// We found this extension in the installation directory, so set the archive path
// property and add to the final set.
File extDir = propFile.getParentFile();
extension.setArchivePath(extDir.getAbsolutePath());
if (extensions.contains(extension)) {
log.error(
"Skipping duplicate extension \"" + extension.getName() + "\" found at " +
extension.getInstallPath());
}
extensions.add(extension);
}
}
/**
* 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 file " + file);
if (file == null) {
log.error("Install file cannot be null");
return false;
}
ExtensionDetails extension = getExtension(file, false);
if (extension == null) {
Msg.showError(ExtensionUtils.class, null, "Error Installing Extension",
file.getAbsolutePath() + " does not point to a valid ghidra extension");
return false;
}
if (checkForConflictWithDevelopmentExtension(extension)) {
return false;
}
if (checkForDuplicateExtensions(extension)) {
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(doRunInstallTask(extension, file, monitor));
});
boolean success = installed.get();
if (success) {
log.trace("Finished installing " + file);
}
else {
log.trace("Failed to install " + file);
}
return success;
}
private static boolean doRunInstallTask(ExtensionDetails extension, File file,
TaskMonitor monitor) {
try {
if (file.isFile()) {
return unzipToInstallationFolder(extension, file, monitor);
}
return copyToInstallationFolder(file, monitor);
}
catch (CancelledException e) {
log.info("Extension installation cancelled by user");
}
catch (IOException e) {
Msg.showError(ExtensionUtils.class, null, "Error Installing Extension",
"Unexpected error installing extension", e);
}
return false;
}
/**
* 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 = getAllInstalledExtensions();
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
return false;
}
log.trace("Duplicate extensions found by name '" + newExtension.getName() + "'");
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(ExtensionUtils.class, null, "Duplicate Extensions Found", buffy.toString());
}
private static void reportDuplicateExtensionsWhenLoading(String name,
List<ExtensionDetails> extensions) {
ExtensionDetails loadedExtension = extensions.get(0);
File loadedInstallDir = loadedExtension.getInstallDir();
for (int i = 1; i < extensions.size(); i++) {
ExtensionDetails duplicate = extensions.get(i);
log.info("Duplicate extension found '" + name + "'. Keeping extension from " +
loadedInstallDir + ". Skipping extension found at " +
duplicate.getInstallDir());
}
}
private static boolean checkForConflictWithDevelopmentExtension(ExtensionDetails newExtension) {
Extensions extensions = getAllInstalledExtensions();
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
return false;
}
for (ExtensionDetails extension : matches) {
if (extension.isInstalledInInstallationFolder()) {
OkDialog.showError("Duplicate Extensions Found",
"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());
return true;
}
}
return false;
}
/**
* Returns true if the given file or directory is a valid ghidra extension.
* <p>
* Note: This means that the zip or directory contains an extension.properties file.
*
* @param file the zip or directory to inspect
* @return true if the given file represents a valid extension
*/
public static boolean isExtension(File file) {
return getExtension(file, true) != null;
}
private static ExtensionDetails getExtension(File file, boolean quiet) {
if (file == null) {
log.error("Cannot get an extension; null file");
return null;
}
try {
return tryToGetExtension(file);
}
catch (IOException e) {
if (quiet) {
log.trace("Exception trying to read an extension from " + file, e);
}
else {
log.error("Exception trying to read an extension from " + file, e);
}
}
return null;
}
private static ExtensionDetails tryToGetExtension(File file) throws IOException {
if (file == null) {
log.error("Cannot get an extension; null file");
return null;
}
if (file.isDirectory() && file.canRead()) {
File[] files = file.listFiles(f -> f.getName().equals(PROPERTIES_FILE_NAME));
if (files != null && files.length == 1) {
return tryToLoadExtensionFromProperties(files[0]);
}
}
// If the given file is a zip, it's an extension if there's an extension.properties
// file at the TOP LEVEL ONLY; we don't want to search for nested property files (this
// would cause us to match things like the main ghidra distribution zip file.
// eg: DatabaseTools/extension.properties is valid
// DatabaseTools/foo/extension.properties is not.
if (isZip(file)) {
try (ZipFile zipFile = new ZipFile(file)) {
Properties props = getProperties(zipFile);
if (props != null) {
return createExtensionDetails(props);
}
throw new IOException("No extension.properties file found in zip");
}
}
return null;
}
/**
* Returns true if the given file is a valid .zip archive.
*
* @param file the file to test
* @return true if file is a valid zip
*/
private static boolean isZip(File file) {
if (file == null) {
log.error("Cannot check for extension zip; null file");
return false;
}
if (file.isDirectory()) {
return false;
}
if (file.length() < 4) {
return false;
}
try (DataInputStream in =
new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {
int test = in.readInt();
return test == ZIPFILE;
}
catch (IOException e) {
log.trace("Unable to check if file is a zip file: " + file + ". " + e.getMessage());
return false;
}
}
/**
* Returns a list of files representing all the <code>extension.properties</code> files found
* under a given directory. This will only search the immediate children of the given directory.
* <p>
* Searching the child directories of a directory allows clients to pick an extension parent
* directory that contains multiple extension directories.
*
* @param installDir the directory that contains extension subdirectories
* @return list of extension.properties files
*/
private static List<File> findExtensionPropertyFiles(File installDir) {
List<File> results = new ArrayList<>();
FileUtilities.forEachFile(installDir, f -> {
if (!f.isDirectory() || f.getName().equals("Skeleton")) {
return;
}
File pf = getPropertyFile(f);
if (pf != null) {
results.add(pf);
}
});
return results;
}
/**
* Returns an extension.properties or extension.properties.uninstalled file if the given
* directory contains one.
*
* @param dir the directory to search
* @return the file, or null if doesn't exist
*/
private static File getPropertyFile(File dir) {
File f = new File(dir, PROPERTIES_FILE_NAME_UNINSTALLED);
if (f.exists()) {
return f;
}
f = new File(dir, PROPERTIES_FILE_NAME);
if (f.exists()) {
return f;
}
return null;
}
private static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
File file = resourceFile.getFile(false);
if (!isZip(file)) {
return null;
}
try (ZipFile zipFile = new ZipFile(file)) {
Properties props = getProperties(zipFile);
if (props != null) {
ExtensionDetails extension = createExtensionDetails(props);
extension.setArchivePath(file.getAbsolutePath());
return extension;
}
}
catch (IOException e) {
log.error(
"Unable to read zip file to get extension properties: " + file, e);
}
return null;
}
private static Properties getProperties(ZipFile zipFile) throws IOException {
Properties props = null;
Enumeration<ZipArchiveEntry> zipEntries = zipFile.getEntries();
while (zipEntries.hasMoreElements()) {
ZipArchiveEntry entry = zipEntries.nextElement();
Properties nextProperties = getProperties(zipFile, entry);
if (nextProperties != null) {
if (props != null) {
throw new IOException(
"Zip file contains multiple extension properties files");
}
props = nextProperties;
}
}
return props;
}
private static Properties getProperties(ZipFile zipFile, ZipArchiveEntry entry)
throws IOException {
// We only search for the property file at the top level
String path = entry.getName();
List<String> parts = FileUtilities.pathToParts(path);
if (parts.size() != 2) { // require 2 parts: dir name / props file
return null;
}
if (!entry.getName().endsWith(PROPERTIES_FILE_NAME)) {
return null;
}
InputStream propFile = zipFile.getInputStream(entry);
Properties prop = new Properties();
prop.load(propFile);
return prop;
}
/**
* Copies the given folder to the extension install location. Any existing folder at that
* location will be deleted.
* <p>
* Note: Any existing folder with the same name will be overwritten.
*
* @param sourceFolder the extension folder
* @param monitor the task monitor
* @return true if successful
* @throws IOException if the delete or copy fails
* @throws CancelledException if the user cancels the copy
*/
private static boolean copyToInstallationFolder(File sourceFolder, TaskMonitor monitor)
throws IOException, CancelledException {
log.trace("Copying extension from " + sourceFolder);
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile installDir = layout.getExtensionInstallationDirs().get(0);
File installDirRoot = installDir.getFile(false);
File newDir = new File(installDirRoot, sourceFolder.getName());
if (hasExistingExtension(newDir, monitor)) {
return false;
}
FileUtilities.copyDir(sourceFolder, newDir, monitor);
return true;
}
private static boolean hasExistingExtension(File extensionFolder, TaskMonitor monitor) {
if (extensionFolder.exists()) {
Msg.showWarn(ExtensionUtils.class, null, "Duplicate Extension Folder",
"Attempting to install a new extension over an existing directory.\n" +
"Either remove the extension for that directory from the UI\n" +
"or close Ghidra and delete the directory and try installing again.\n\n" +
"Directory: " + extensionFolder);
return true;
}
return false;
}
/**
* Unpacks a given zip file to {@link ApplicationLayout#getExtensionInstallationDirs}. The
* file permissions in the original zip will be retained.
* <p>
* Note: This method uses the Apache zip files since they keep track of permissions info;
* the built-in java objects (e.g., ZipEntry) do not.
*
* @param extension the extension
* @param file the zip file to unpack
* @param monitor the task monitor
* @return true if successful
* @throws IOException if any part of the unzipping fails, or if the target location is invalid
* @throws CancelledException if the user cancels the unzip
* @throws IOException if error unzipping zip file
*/
private static boolean unzipToInstallationFolder(ExtensionDetails extension, File file,
TaskMonitor monitor)
throws CancelledException, IOException {
log.trace("Unzipping extension from " + file);
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile installDir = layout.getExtensionInstallationDirs().get(0);
File installDirRoot = installDir.getFile(false);
File destinationFolder = new File(installDirRoot, extension.getName());
if (hasExistingExtension(destinationFolder, monitor)) {
return false;
}
try (ZipFile zipFile = new ZipFile(file)) {
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
monitor.checkCancelled();
ZipArchiveEntry entry = entries.nextElement();
String filePath = installDir + File.separator + entry.getName();
File destination = new File(filePath);
if (entry.isDirectory()) {
destination.mkdirs();
}
else {
writeZipEntryToFile(zipFile, entry, destination);
}
}
}
return true;
}
private static void writeZipEntryToFile(ZipFile zFile, ZipArchiveEntry entry, File destination)
throws IOException {
try (OutputStream outputStream =
new BufferedOutputStream(new FileOutputStream(destination))) {
// Create the file at the new location...
IOUtils.copy(zFile.getInputStream(entry), outputStream);
// ...and update its permissions. But only continue if the zip was created on a unix
//platform. If not, we cannot use the posix libraries to set permissions.
if (entry.getPlatform() != ZipArchiveEntry.PLATFORM_UNIX) {
return;
}
int mode = entry.getUnixMode();
if (mode != 0) { // 0 indicates non-unix platform
Set<PosixFilePermission> perms = getPermissions(mode);
try {
Files.setPosixFilePermissions(destination.toPath(), perms);
}
catch (UnsupportedOperationException e) {
// Need to catch this, as Windows does not support the posix call. This is not
// an error, however, and should just silently fail.
}
}
}
}
private static ExtensionDetails tryToLoadExtensionFromProperties(File file) throws IOException {
Properties props = new Properties();
try (InputStream in = new FileInputStream(file.getAbsolutePath())) {
props.load(in);
return createExtensionDetails(props);
}
}
private static ExtensionDetails createExtensionFromProperties(File file) {
try {
return tryToLoadExtensionFromProperties(file);
}
catch (IOException e) {
log.error("Error loading extension properties from " + file.getAbsolutePath(), e);
return null;
}
}
private static ExtensionDetails createExtensionDetails(Properties props) {
String name = props.getProperty("name");
String desc = props.getProperty("description");
String author = props.getProperty("author");
String date = props.getProperty("createdOn");
String version = props.getProperty("version");
return new ExtensionDetails(name, desc, author, date, version);
}
/**
* Uninstalls a given extension.
*
* @param extension the extension to uninstall
* @return true if successfully uninstalled
*/
private static boolean removeExtension(ExtensionDetails extension) {
if (extension == null) {
log.error("Extension to uninstall cannot be null");
return false;
}
File installDir = extension.getInstallDir();
if (installDir == null) {
log.error("Extension installation path is not set; unable to delete files");
return false;
}
if (FileUtilities.deleteDir(installDir)) {
extension.setInstallDir(null);
return true;
}
return false;
}
/**
* Converts Unix permissions to a set of {@link PosixFilePermission}s.
*
* @param unixMode integer representation of file permissions
* @return set of POSIX file permissions
*/
private static Set<PosixFilePermission> getPermissions(int unixMode) {
Set<PosixFilePermission> permissions = new HashSet<>();
if ((unixMode & 0400) != 0) {
permissions.add(PosixFilePermission.OWNER_READ);
}
if ((unixMode & 0200) != 0) {
permissions.add(PosixFilePermission.OWNER_WRITE);
}
if ((unixMode & 0100) != 0) {
permissions.add(PosixFilePermission.OWNER_EXECUTE);
}
if ((unixMode & 0040) != 0) {
permissions.add(PosixFilePermission.GROUP_READ);
}
if ((unixMode & 0020) != 0) {
permissions.add(PosixFilePermission.GROUP_WRITE);
}
if ((unixMode & 0010) != 0) {
permissions.add(PosixFilePermission.GROUP_EXECUTE);
}
if ((unixMode & 0004) != 0) {
permissions.add(PosixFilePermission.OTHERS_READ);
}
if ((unixMode & 0002) != 0) {
permissions.add(PosixFilePermission.OTHERS_WRITE);
}
if ((unixMode & 0001) != 0) {
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
}
return permissions;
}
/**
* A collection of all extensions found. This class provides methods processing duplicates and
* managing extensions marked for removal.
*/
private static class Extensions {
private Map<String, List<ExtensionDetails>> extensionsByName = new HashMap<>();
void add(ExtensionDetails e) {
extensionsByName.computeIfAbsent(e.getName(), n -> new ArrayList<>()).add(e);
}
Set<ExtensionDetails> getActiveExtensions() {
return extensionsByName.values()
.stream()
.map(list -> list.get(0))
.filter(ext -> !ext.isPendingUninstall())
.collect(Collectors.toSet());
}
List<ExtensionDetails> getMatchingExtensions(ExtensionDetails extension) {
return extensionsByName.computeIfAbsent(extension.getName(), name -> List.of());
}
void cleanupExtensionsMarkedForRemoval() {
Set<String> names = new HashSet<>(extensionsByName.keySet());
for (String name : names) {
List<ExtensionDetails> extensions = extensionsByName.get(name);
Iterator<ExtensionDetails> it = extensions.iterator();
while (it.hasNext()) {
ExtensionDetails extension = it.next();
if (!extension.isPendingUninstall()) {
continue;
}
if (!removeExtension(extension)) {
log.error("Error removing extension: " + extension.getInstallPath());
}
it.remove();
}
if (extensions.isEmpty()) {
extensionsByName.remove(name);
}
}
}
void reportDuplicateExtensions() {
Set<Entry<String, List<ExtensionDetails>>> entries = extensionsByName.entrySet();
for (Entry<String, List<ExtensionDetails>> entry : entries) {
List<ExtensionDetails> list = entry.getValue();
if (list.size() == 1) {
continue;
}
reportDuplicateExtensionsWhenLoading(entry.getKey(), list);
}
}
/**
* Returns all unique (no duplicates) extensions that the application is aware of
* @return the extensions
*/
Set<ExtensionDetails> get() {
return extensionsByName.values()
.stream()
.map(list -> list.get(0))
.collect(Collectors.toSet());
}
}
}

View file

@ -0,0 +1,320 @@
/* ###
* 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.tool;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import org.jdom.Element;
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.ExtensionDetails;
import ghidra.framework.project.extensions.ExtensionUtils;
import ghidra.util.NumericUtilities;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.xml.XmlUtilities;
import utilities.util.FileUtilities;
/**
* A class to manage saving and restoring of known extension used by this tool.
*/
class ExtensionManager {
private static final String EXTENSION_ATTRIBUTE_NAME_ENCODED = "ENCODED_NAME";
private static final String EXTENSION_ATTRIBUTE_NAME = "NAME";
private static final String EXTENSIONS_XML_NAME = "EXTENSIONS";
private static final String EXTENSION_ELEMENT_NAME = "EXTENSION";
private PluginTool tool;
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
ExtensionManager(PluginTool tool) {
this.tool = tool;
}
void checkForNewExtensions() {
if (newExtensionPlugins.isEmpty()) {
return;
}
propmtToConfigureNewPlugins(newExtensionPlugins);
newExtensionPlugins.clear();
}
private void propmtToConfigureNewPlugins(Set<Class<?>> 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?");
if (option == OptionDialog.YES_OPTION) {
List<PluginDescription> pluginDescriptions = getPluginDescriptions(plugins);
PluginInstallerDialog pluginInstaller = new PluginInstallerDialog("New Plugins Found!",
tool, new PluginConfigurationModel(tool), pluginDescriptions);
tool.showDialog(pluginInstaller);
}
}
void saveToXml(Element xml) {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
Element extensionsParent = new Element(EXTENSIONS_XML_NAME);
for (ExtensionDetails ext : installedExtensions) {
Element child = new Element(EXTENSION_ELEMENT_NAME);
String name = ext.getName();
if (XmlUtilities.hasInvalidXMLCharacters(name)) {
child.setAttribute(EXTENSION_ATTRIBUTE_NAME_ENCODED, NumericUtilities
.convertBytesToString(name.getBytes(StandardCharsets.UTF_8)));
}
else {
child.setAttribute(EXTENSION_ATTRIBUTE_NAME, name);
}
extensionsParent.addContent(child);
}
xml.addContent(extensionsParent);
}
void restoreFromXml(Element xml) {
Set<ExtensionDetails> installedExtensions = getExtensions();
if (installedExtensions.isEmpty()) {
return;
}
Set<String> knownExtensionNames = getKnownExtensions(xml);
Set<ExtensionDetails> newExtensions = new HashSet<>(installedExtensions);
for (ExtensionDetails ext : installedExtensions) {
if (knownExtensionNames.contains(ext.getName())) {
newExtensions.remove(ext);
}
}
// Get a list of all plugins contained in those extensions. If there are none, then either
// none of the extensions has any plugins, or Ghidra hasn't been restarted since installing
// the extension(s), so none of the plugin classes have been loaded. In either case, there
// is nothing more to do.
Set<Class<?>> newPlugins = findLoadedPlugins(newExtensions);
newExtensionPlugins.addAll(newPlugins);
}
private Set<ExtensionDetails> getExtensions() {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
return installedExtensions.stream()
.filter(e -> !e.isInstalledInInstallationFolder())
.collect(Collectors.toSet());
}
private Set<String> getKnownExtensions(Element xml) {
Set<String> knownExtensionNames = new HashSet<>();
Element extensionsParent = xml.getChild(EXTENSIONS_XML_NAME);
if (extensionsParent == null) {
return knownExtensionNames;
}
Iterator<?> it = extensionsParent.getChildren(EXTENSION_ELEMENT_NAME).iterator();
while (it.hasNext()) {
Element child = (Element) it.next();
String encodedValue = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME_ENCODED);
if (encodedValue != null) {
byte[] bytes = NumericUtilities.convertStringToBytes(encodedValue);
String decoded = new String(bytes, StandardCharsets.UTF_8);
knownExtensionNames.add(decoded);
}
else {
String name = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME);
knownExtensionNames.add(name);
}
}
return knownExtensionNames;
}
/**
* 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.
* <p>
* Note that this method does not take path/package information into account when finding
* plugins; in the example above, if there is more than one plugin with the name "FooPlugin",
* only one will be found (the one found is not guaranteed to be the first).
*
* @param plugins the list of plugin classes to search for
* @return list of plugin descriptions
*/
private List<PluginDescription> getPluginDescriptions(Set<Class<?>> plugins) {
// First define the list of plugin descriptions to return
List<PluginDescription> descriptions = new ArrayList<>();
// Get all plugins that have been loaded
PluginsConfiguration pluginsConfiguration = tool.getPluginsConfiguration();
List<PluginDescription> allPluginDescriptions =
pluginsConfiguration.getManagedPluginDescriptions();
// see if an entry exists in the list of all loaded plugins
for (Class<?> plugin : plugins) {
String pluginName = plugin.getSimpleName();
Optional<PluginDescription> desc = allPluginDescriptions.stream()
.filter(d -> (pluginName.equals(d.getName())))
.findAny();
if (desc.isPresent()) {
descriptions.add(desc.get());
}
}
return descriptions;
}
private static Set<Class<?>> findLoadedPlugins(Set<ExtensionDetails> extensions) {
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);
extensionPlugins.addAll(classes);
}
return extensionPlugins;
}
private static Set<PluginPath> getPluginPaths() {
Set<PluginPath> paths = new HashSet<>();
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
for (Class<? extends Plugin> plugin : plugins) {
paths.add(new PluginPath(plugin));
}
return paths;
}
/**
* Finds all plugin classes loaded from a particular extension folder.
* <p>
* This uses the {@link ClassSearcher} to find all <code>Plugin.class</code> objects on the
* classpath. For each class, the original resource file is compared against the
* given extension folder and the jar files for that extension.
*
* @param dir the directory to search, or a jar file
* @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,
Set<PluginPath> pluginPaths) {
Set<Class<?>> result = new HashSet<>();
// Find any jar files in the directory provided
Set<String> jarPaths = getJarPaths(dir);
// Now get all Plugin.class file paths and see if any of them were loaded from one of the
// extension the given extension directory
for (PluginPath pluginPath : pluginPaths) {
if (pluginPath.isFrom(dir)) {
result.add(pluginPath.getPluginClass());
continue;
}
for (String jarPath : jarPaths) {
if (pluginPath.isFrom(jarPath)) {
result.add(pluginPath.getPluginClass());
}
}
}
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;
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(String jarPath) {
return pluginLocation.contains(jarPath);
}
Class<? extends Plugin> getPluginClass() {
return pluginClass;
}
@Override
public String toString() {
return Json.toString(this);
}
}
}

View file

@ -17,7 +17,7 @@ package ghidra.framework.project.tool;
import ghidra.framework.main.ApplicationLevelOnlyPlugin;
import ghidra.framework.plugintool.Plugin;
import ghidra.framework.plugintool.util.PluginsConfiguration;
import ghidra.framework.plugintool.PluginsConfiguration;
/**
* A configuration that allows all general plugins and application plugins. Plugins that may only

View file

@ -15,10 +15,6 @@
*/
package ghidra.framework.project.tool;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.jdom.Element;
import docking.ActionContext;
@ -30,13 +26,9 @@ import docking.widgets.OptionDialog;
import ghidra.app.util.FileOpenDropHandler;
import ghidra.framework.model.Project;
import ghidra.framework.model.ToolTemplate;
import ghidra.framework.options.PreferenceState;
import ghidra.framework.plugintool.PluginConfigurationModel;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.dialog.*;
import ghidra.framework.plugintool.util.*;
import ghidra.framework.plugintool.PluginsConfiguration;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
/**
* Tool created by the workspace when the user chooses to create a new
@ -47,16 +39,14 @@ public class GhidraTool extends PluginTool {
private static final String NON_AUTOSAVE_SAVE_TOOL_TITLE = "Save Tool?";
// Preference category stored in the tools' xml file, indicating which extensions
// this tool is aware of. This is used to recognize when new extensions have been
// installed that the user should be made aware of.
public static final String EXTENSIONS_PREFERENCE_NAME = "KNOWN_EXTENSIONS";
public static boolean autoSave = true;
private FileOpenDropHandler fileOpenDropHandler;
private DockingAction configureToolAction;
private ExtensionManager extensionManager;
private boolean hasBeenShown;
/**
* Construct a new Ghidra Tool.
*
@ -77,6 +67,18 @@ public class GhidraTool extends PluginTool {
super(project, template);
}
/**
* We need to do this here, since our parent constructor calls methods on us that need the
* extension manager.
* @return the extension manager
*/
private ExtensionManager getExtensionManager() {
if (extensionManager == null) {
extensionManager = new ExtensionManager(this);
}
return extensionManager;
}
@Override
protected DockingWindowManager createDockingWindowManager(boolean isDockable, boolean hasStatus,
boolean isModal) {
@ -122,6 +124,31 @@ public class GhidraTool extends PluginTool {
winMgr.restoreWindowDataFromXml(rootElement);
}
@Override
public Element saveToXml(boolean includeConfigState) {
Element xml = super.saveToXml(includeConfigState);
getExtensionManager().saveToXml(xml);
return xml;
}
@Override
protected boolean restoreFromXml(Element root) {
boolean success = super.restoreFromXml(root);
getExtensionManager().restoreFromXml(root);
return success;
}
@Override
public void setVisible(boolean visible) {
if (visible) {
if (!hasBeenShown) { // first time being shown
getExtensionManager().checkForNewExtensions();
}
hasBeenShown = true;
}
super.setVisible(visible);
}
@Override
public boolean shouldSave() {
if (autoSave) {
@ -207,175 +234,6 @@ public class GhidraTool extends PluginTool {
}
protected void showConfig() {
// if (hasUnsavedData()) {
// OptionDialog.showWarningDialog( getToolFrame(),"Configure Not Allowed!",
// "The tool has unsaved data. Configuring the tool can potentially lose\n"+
// "data. Therefore, this operation is not allowed with unsaved data.\n\n"+
// "Please save your data before configuring the tool.");
// return;
// }
showConfig(true, false);
}
/**
* Looks for extensions that have been installed since the last time this tool
* was launched. If any are found, and if those extensions contain plugins, the user is
* notified and given the chance to install them.
*
*/
public void checkForNewExtensions() {
// 1. First remove any extensions that are in the tool preferences that are no longer
// installed. This will happen if the user installs an extension, launches
// a tool, then uninstalls the extension.
removeUninstalledExtensions();
// 2. Now figure out which extensions have been added.
Set<ExtensionDetails> newExtensions =
ExtensionUtils.getExtensionsInstalledSinceLastToolLaunch(this);
// 3. Get a list of all plugins contained in those extensions. If there are none, then
// either none of the extensions has any plugins, or Ghidra hasn't been restarted since
// installing the extension(s), so none of the plugin classes have been loaded. In
// either case, there is nothing more to do.
List<Class<?>> newPlugins = PluginUtils.findLoadedPlugins(newExtensions);
if (newPlugins.isEmpty()) {
return;
}
// 4. Notify the user there are new plugins.
int option = OptionDialog.showYesNoDialog(getActiveWindow(), "New Plugins Found!",
"New extension plugins detected. Would you like to configure them?");
if (option == OptionDialog.YES_OPTION) {
List<PluginDescription> pluginDescriptions = getPluginDescriptions(this, newPlugins);
PluginInstallerDialog pluginInstaller = new PluginInstallerDialog("New Plugins Found!",
this, new PluginConfigurationModel(this), pluginDescriptions);
showDialog(pluginInstaller);
}
// 5. Update the preference file to reflect the new extensions now known to this tool.
addInstalledExtensions(newExtensions);
}
/**
* 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.
* <p>
* Note that this method does not take path/package information into account when finding
* plugins; in the example above, if there is more than one plugin with the name "FooPlugin",
* only one will be found (the one found is not guaranteed to be the first).
*
* @param tool the current tool
* @param plugins the list of plugin classes to search for
* @return list of plugin descriptions
*/
private List<PluginDescription> getPluginDescriptions(PluginTool tool, List<Class<?>> plugins) {
// First define the list of plugin descriptions to return
List<PluginDescription> retPlugins = new ArrayList<>();
// Get all plugins that have been loaded
PluginsConfiguration pluginClassManager = getPluginsConfiguration();
List<PluginDescription> allPluginDescriptions =
pluginClassManager.getManagedPluginDescriptions();
// see if an entry exists in the list of all loaded plugins
for (Class<?> plugin : plugins) {
String pluginName = plugin.getSimpleName();
Optional<PluginDescription> desc = allPluginDescriptions.stream()
.filter(d -> (pluginName.equals(d.getName())))
.findAny();
if (desc.isPresent()) {
retPlugins.add(desc.get());
}
}
return retPlugins;
}
/**
* Removes any extensions in the tool preferences that are no longer installed.
*/
private void removeUninstalledExtensions() {
try {
// Get all installed extensions
Set<ExtensionDetails> installedExtensions =
ExtensionUtils.getInstalledExtensions(false);
List<String> installedExtensionNames =
installedExtensions.stream().map(ext -> ext.getName()).collect(Collectors.toList());
// Get the list of extensions in the tool preference state
DockingWindowManager dockingWindowManager =
DockingWindowManager.getInstance(getToolFrame());
PreferenceState state = getExtensionPreferences(dockingWindowManager);
String[] extNames = state.getStrings(EXTENSIONS_PREFERENCE_NAME, new String[0]);
List<String> preferenceExtensionNames = new ArrayList<>(Arrays.asList(extNames));
// Now see if any extensions are in the current preferences that are NOT in the installed extensions
// list. Those are the ones we need to remove.
for (Iterator<String> i = preferenceExtensionNames.iterator(); i.hasNext();) {
String extName = i.next();
if (!installedExtensionNames.contains(extName)) {
i.remove();
}
}
// Finally, put the new extension list in the preferences object
state.putStrings(EXTENSIONS_PREFERENCE_NAME,
preferenceExtensionNames.toArray(new String[preferenceExtensionNames.size()]));
dockingWindowManager.putPreferenceState(EXTENSIONS_PREFERENCE_NAME, state);
}
catch (ExtensionException e) {
// This is a problem but isn't catastrophic. Just warn the user and continue.
Msg.warn(this, "Couldn't retrieve installed extensions!", e);
}
}
/**
* Updates the preferences for this tool with a set of new extensions.
*
* @param newExtensions the extensions to add
*/
private void addInstalledExtensions(Set<ExtensionDetails> newExtensions) {
DockingWindowManager dockingWindowManager =
DockingWindowManager.getInstance(getToolFrame());
// Get the current preference object. We need to get the existing prefs so we can add our
// new extensions to them. If the extensions category doesn't exist yet, just create one.
PreferenceState state = getExtensionPreferences(dockingWindowManager);
// Now get the list of extensions already in the prefs...
String[] extNames = state.getStrings(EXTENSIONS_PREFERENCE_NAME, new String[0]);
// ...and parse the passed-in extension list to get just the names of the extensions to add.
List<String> extensionNamesToAdd =
newExtensions.stream().map(ext -> ext.getName()).collect(Collectors.toList());
// Finally add them together and update the preference state.
String[] allPreferences = ArrayUtils.addAll(extNames,
extensionNamesToAdd.toArray(new String[extensionNamesToAdd.size()]));
state.putStrings(EXTENSIONS_PREFERENCE_NAME, allPreferences);
dockingWindowManager.putPreferenceState(EXTENSIONS_PREFERENCE_NAME, state);
}
/**
* Return the extensions portion of the preferences object.
*
* @param dockingWindowManager the docking window manager
* @return the extensions portion of the preference state, or a new preference state object if no extension section exists
*/
private PreferenceState getExtensionPreferences(DockingWindowManager dockingWindowManager) {
PreferenceState state = dockingWindowManager.getPreferenceState(EXTENSIONS_PREFERENCE_NAME);
if (state == null) {
state = new PreferenceState();
}
return state;
}
}

View file

@ -30,7 +30,6 @@ import ghidra.framework.data.*;
import ghidra.framework.main.AppInfo;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.PluginEvent;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.preferences.Preferences;
import ghidra.framework.protocol.ghidra.GetUrlContentTypeTask;
@ -169,27 +168,6 @@ class ToolServicesImpl implements ToolServices {
return toolChest;
}
@Override
public void displaySimilarTool(PluginTool tool, DomainFile domainFile, PluginEvent event) {
PluginTool[] similarTools = getSameNamedRunningTools(tool);
PluginTool matchingTool = findToolUsingFile(similarTools, domainFile);
if (matchingTool != null) {
// Bring the matching tool forward.
matchingTool.toFront();
}
else {
// Create a new tool and pop it up.
Workspace workspace = toolManager.getActiveWorkspace();
matchingTool = workspace.runTool(tool.getToolTemplate(true));
matchingTool.setVisible(true);
matchingTool.acceptDomainFiles(new DomainFile[] { domainFile });
}
// Fire the indicated event in the tool.
matchingTool.firePluginEvent(event);
}
private static DefaultLaunchMode getDefaultLaunchMode() {
DefaultLaunchMode defaultLaunchMode = DefaultLaunchMode.DEFAULT;
FrontEndTool frontEndTool = AppInfo.getFrontEndTool();

View file

@ -79,14 +79,9 @@ class WorkspaceImpl implements Workspace {
PluginTool tool = toolManager.getTool(this, template);
if (tool != null) {
tool.setVisible(true);
if (tool instanceof GhidraTool) {
GhidraTool gTool = (GhidraTool) tool;
gTool.checkForNewExtensions();
}
runningTools.add(tool);
// alert the tool manager that we changed
// alert the tool manager that we have changed
toolManager.setWorkspaceChanged(this);
toolManager.fireToolAddedEvent(this, tool);
}
@ -161,6 +156,7 @@ class WorkspaceImpl implements Workspace {
String defaultTool = System.getProperty("ghidra.defaulttool");
if (defaultTool != null && !defaultTool.equals("")) {
PluginTool tool = toolManager.getTool(defaultTool);
tool.setVisible(isActive);
runningTools.add(tool);
toolManager.fireToolAddedEvent(this, tool);
return;
@ -175,27 +171,23 @@ class WorkspaceImpl implements Workspace {
}
PluginTool tool = toolManager.getTool(toolName);
if (tool != null) {
tool.setVisible(isActive);
if (tool instanceof GhidraTool) {
GhidraTool gTool = (GhidraTool) tool;
gTool.checkForNewExtensions();
}
boolean hadChanges = tool.hasConfigChanged();
tool.restoreWindowingDataFromXml(element);
Element toolDataElem = element.getChild("DATA_STATE");
tool.restoreDataStateFromXml(toolDataElem);
if (hadChanges) {
// restore the dirty state, which is cleared by the restoreDataState call
tool.setConfigChanged(true);
}
runningTools.add(tool);
toolManager.fireToolAddedEvent(this, tool);
if (tool == null) {
continue;
}
tool.setVisible(isActive);
boolean hadChanges = tool.hasConfigChanged();
tool.restoreWindowingDataFromXml(element);
Element toolDataElem = element.getChild("DATA_STATE");
tool.restoreDataStateFromXml(toolDataElem);
if (hadChanges) {
// restore the dirty state, which is cleared by the restoreDataState call
tool.setConfigChanged(true);
}
runningTools.add(tool);
toolManager.fireToolAddedEvent(this, tool);
}
}

View file

@ -15,8 +15,6 @@
*/
package ghidra.framework.plugintool;
import ghidra.framework.plugintool.util.PluginsConfiguration;
/**
* A dummy version of {@link PluginTool} that tests can use when they need an instance of
* the PluginTool, but do not wish to use a real version

View file

@ -0,0 +1,747 @@
/* ###
* 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 static org.junit.Assert.*;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.*;
import docking.DialogComponentProvider;
import docking.test.AbstractDockingTest;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
import utility.function.ExceptionalCallback;
import utility.module.ModuleUtilities;
/**
* Tests for the {@link ExtensionUtils} class.
*/
public class ExtensionUtilsTest extends AbstractDockingTest {
private static final String BUILD_FOLDER_NAME = "TestExtensionParentDir";
private static final String TEST_EXT_NAME = "test";
private ApplicationLayout appLayout;
/*
* Create dummy archive and installation folders in the temp space that we can populate
* with extensions.
*/
@Before
public void setup() throws IOException {
// to see tracing; must set the 'console' appender to trace to see these
// Configurator.setLevel("ghidra.framework.project.extensions", Level.TRACE);
setErrorGUIEnabled(false);
appLayout = Application.getApplicationLayout();
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
FileUtilities.deleteDir(installDir.getFile(false));
}
createExtensionDirs();
}
private static <E extends Exception> void errorsExpected(ExceptionalCallback<E> c)
throws Exception {
try {
setErrorsExpected(true);
c.call();
}
finally {
setErrorsExpected(false);
}
}
@After
public void tearDown() {
sleep(1000);
}
/*
* Verifies that we can install an extension from a .zip file.
*/
@Test
public void testInstallExtensionFromZip() throws IOException {
// Create an extension and install it.
File file = createExtensionZip(TEST_EXT_NAME);
ExtensionUtils.install(file);
// Verify there is something in the installation directory and it has the correct name
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
}
/*
* Verifies that we can install an extension from a folder.
*/
@Test
public void testInstallArchiveExtensionFromFolder() throws IOException {
// Create an extension and install it.
File file = createExtensionFolderInArchiveDir();
ExtensionUtils.install(file);
// Verify the extension is in the install folder and has the correct name
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
}
@Test
public void testIsExtension_Zip_ValidZip() throws IOException {
File zipFile1 = createExtensionZip(TEST_EXT_NAME);
assertTrue(ExtensionUtils.isExtension(zipFile1));
}
@Test
public void testIsExtension_Zip_InvalidZip() throws Exception {
errorsExpected(() -> {
File zipFile2 = createNonExtensionZip(TEST_EXT_NAME);
assertFalse(ExtensionUtils.isExtension(zipFile2));
});
}
/*
* Verifies that we can recognize when a directory represents an extension.
* <p>
* Note: The presence of an extensions.properties file is the difference.
*/
@Test
public void testIsExtension_Folder() throws Exception {
File extDir = createTempDirectory("TestExtFolder");
new File(extDir, "extension.properties").createNewFile();
assertTrue(ExtensionUtils.isExtension(extDir));
errorsExpected(() -> {
File nonExtDir = createTempDirectory("TestNonExtFolder");
assertFalse(ExtensionUtils.isExtension(nonExtDir));
});
}
@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));
});
}
@Test
public void testInstallExtensionFromArchive() throws Exception {
File zipFile = createExtensionZip(TEST_EXT_NAME);
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
extension.setArchivePath(zipFile.getAbsolutePath());
String ghidraVersion = Application.getApplicationVersion();
extension.setVersion(ghidraVersion);
assertTrue(ExtensionUtils.installExtensionFromArchive(extension));
}
@Test
public void testInstallExtensionFromZipArchive_VersionMismatch_Cancel() throws Exception {
File zipFile = createExtensionZip(TEST_EXT_NAME, "v2");
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
extension.setArchivePath(zipFile.getAbsolutePath());
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
waitForDialogComponent("Extension Version Mismatch");
pressButtonByText(confirmDialog, "Cancel");
assertFalse(didInstall.get());
}
@Test
public void testInstallExtensionFromZipArchive_VersionMismatch_Continue() throws Exception {
File zipFile = createExtensionZip(TEST_EXT_NAME, "v2");
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
extension.setArchivePath(zipFile.getAbsolutePath());
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
waitForDialogComponent("Extension Version Mismatch");
pressButtonByText(confirmDialog, "Install Anyway");
assertFalse(didInstall.get());
}
@Test
public void testInstallExtensionFromZipArchive_NullVersion() throws Exception {
File zipFile = createExtensionZip(TEST_EXT_NAME, null);
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
extension.setVersion(null);
extension.setArchivePath(zipFile.getAbsolutePath());
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
waitForDialogComponent("Extension Version Mismatch");
pressButtonByText(confirmDialog, "Cancel");
assertFalse(didInstall.get());
}
@Test
public void testMarkForUninstall_ClearMark() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
extension.markForUninstall();
checkMarkForUninstall(extension);
assertFalse(extension.isInstalled());
// Also test that we can clear the uninstall marker
extension.clearMarkForUninstall();
assertExtensionInstalled(TEST_EXT_NAME);
}
@Test
public void testCleanupUninstalledExtions_WithExtensionMarkedForUninstall() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
extension.markForUninstall();
checkMarkForUninstall(extension);
assertFalse(extension.isInstalled());
// Also test that we can clear the uninstall marker
ExtensionUtils.initializeExtensions();
checkCleanInstall();
}
@Test
public void testCleanupUninstalledExtions_SomeExtensionMarkedForUninstall() throws Exception {
List<File> extensionFolders = createTwoExternalExtensionsInFolder();
assertTrue(ExtensionUtils.install(extensionFolders.get(0)));
assertTrue(ExtensionUtils.install(extensionFolders.get(1)));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 2);
Iterator<ExtensionDetails> it = extensions.iterator();
ExtensionDetails extensionToRemove = it.next();
ExtensionDetails extensionToKeep = it.next();
assertTrue(extensionToRemove.isInstalled());
extensionToRemove.markForUninstall();
checkMarkForUninstall(extensionToRemove);
assertFalse(extensionToRemove.isInstalled());
// Also test that we can clear the uninstall marker
ExtensionUtils.initializeExtensions();
assertExtensionInstalled(extensionToKeep.getName());
}
@Test
public void testCleanupUninstalledExtions_NoExtensionsMarkedForUninstall() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
assertExtensionInstalled(TEST_EXT_NAME);
// This should not uninstall any extensions
ExtensionUtils.initializeExtensions();
assertExtensionInstalled(TEST_EXT_NAME);
}
//=================================================================================================
// Edge Cases
//=================================================================================================
@Test
public void testInstallingNewExtension_SameName_NewVersion() throws Exception {
// install extension Foo with Ghidra version
File buildFolder = createTempDirectory(BUILD_FOLDER_NAME);
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 1);
ExtensionDetails installedExtension = extensions.iterator().next();
// create another extension Foo v2
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
String newVersion = "v2";
File extensionFolder2 =
doCreateExternalExtensionInFolder(buildFolder2, TEST_EXT_NAME, newVersion);
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
waitForDialogComponent("Duplicate Extension");
pressButtonByText(confirmDialog, "Remove Existing");
waitForSwing();
assertFalse(didInstall.get());
checkMarkForUninstall(installedExtension);
// run again after choosing to replace the installed extension
ExtensionUtils.initializeExtensions(); // removed marked extensions
checkCleanInstall();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
});
// no longer an installed extension conflict; now we have a version mismatch
confirmDialog = waitForDialogComponent("Extension Version Mismatch");
pressButtonByText(confirmDialog, "Install Anyway");
waitFor(didInstall);
assertExtensionInstalled(TEST_EXT_NAME, newVersion);
assertExtensionNotInstalled(TEST_EXT_NAME, appVersion);
}
@Test
public void testInstallingNewExtension_SameName_NewVersion_Cancel() throws Exception {
// install extension Foo with Ghidra version
File buildFolder = createTempDirectory(BUILD_FOLDER_NAME);
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
// create another extension Foo v2
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
String newVersion = "v2";
File extensionFolder2 =
doCreateExternalExtensionInFolder(buildFolder2, TEST_EXT_NAME, newVersion);
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
waitForDialogComponent("Duplicate Extension");
pressButtonByText(confirmDialog, "Cancel");
waitForSwing();
assertExtensionInstalled(TEST_EXT_NAME, appVersion);
assertExtensionNotInstalled(TEST_EXT_NAME, newVersion);
}
@Test
public void testInstallingNewExtension_SameName_SaveVersion() throws Exception {
// install extension Foo with Ghidra version
File buildFolder = createTempDirectory(BUILD_FOLDER_NAME);
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 1);
ExtensionDetails installedExtension = extensions.iterator().next();
// create another extension Foo v2
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
String newVersion = appVersion;
File extensionFolder2 =
doCreateExternalExtensionInFolder(buildFolder2, TEST_EXT_NAME, newVersion);
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
waitForDialogComponent("Duplicate Extension");
pressButtonByText(confirmDialog, "Remove Existing");
waitForSwing();
assertFalse(didInstall.get());
checkMarkForUninstall(installedExtension);
// run again after choosing to replace the installed extension
ExtensionUtils.initializeExtensions(); // removed marked extensions
checkCleanInstall();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
});
waitFor(didInstall);
assertExtensionInstalled(TEST_EXT_NAME, newVersion);
assertEquals(1, ExtensionUtils.getInstalledExtensions().size());
}
@Test
public void testInstallingNewExtension_FromZip_ZipHasMultipleExtensions() throws Exception {
// test that we can detect when a zip has more than one extension inside (as determined
// by multiple properties files 1 level down from the root with different folder names
/*
Create a zip file that looks something like this:
/
{Extension Name 1}/
extension.properties
{Extension Name 2}/
extension.properties
*/
errorsExpected(() -> {
File zipFile = createZipWithMultipleExtensions();
assertFalse(ExtensionUtils.install(zipFile));
});
}
@Test
public void testInstallThenUninstallThenReinstallWhenExtensionNameDoesntMatchFolder()
throws Exception {
// This tests a previous failure case where an extension could not be reinstalled if its
// name did not match the folder it was installed into. This could happen because the code
// that installed the extension did not match the code to clear the 'mark for uninstall'
// condition.
String nameProperty = "ExtensionNamedFoo";
File externalFolder = createExtensionWithMismatchingNamePropertyString(nameProperty);
assertTrue(ExtensionUtils.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(nameProperty);
extension.markForUninstall();
checkMarkForUninstall(extension);
assertFalse(extension.isInstalled());
// Also test that we can clear the uninstall marker
extension.clearMarkForUninstall();
assertExtensionInstalled(nameProperty);
}
//==================================================================================================
// Private Methods
//==================================================================================================
private ExtensionDetails assertExtensionInstalled(String name) {
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
Optional<ExtensionDetails> match =
extensions.stream().filter(e -> e.getName().equals(name)).findFirst();
assertTrue("No extension installed named '" + name + "'", match.isPresent());
ExtensionDetails extension = match.get();
assertTrue(extension.isInstalled());
return extension;
}
private ExtensionDetails assertExtensionInstalled(String name, String version) {
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
Optional<ExtensionDetails> match =
extensions.stream().filter(e -> e.getName().equals(name)).findFirst();
assertTrue("No extension installed named '" + name + "'", match.isPresent());
ExtensionDetails extension = match.get();
assertEquals(version, extension.getVersion());
assertTrue(extension.isInstalled());
return extension;
}
private void assertExtensionNotInstalled(String name, String version) {
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
Optional<ExtensionDetails> match =
extensions.stream()
.filter(e -> e.getName().equals(name) && e.getVersion().equals(version))
.findFirst();
assertFalse("Extension should not be installed: '" + name + "'", match.isPresent());
}
/*
* Creates the extension archive and installation directories.
*
* @throws IOException if there's an error creating the directories
*/
private void createExtensionDirs() throws IOException {
ResourceFile extensionDir = appLayout.getExtensionArchiveDir();
if (!extensionDir.exists()) {
if (!extensionDir.mkdir()) {
throw new IOException("Failed to create extension archive directory for test");
}
}
ResourceFile installDir = appLayout.getExtensionInstallationDirs().get(0);
if (!installDir.exists()) {
if (!installDir.mkdir()) {
throw new IOException("Failed to create extension installation directory for test");
}
}
}
/*
* Verifies that the installation folder is empty.
*/
private boolean checkCleanInstall() {
ResourceFile[] files = appLayout.getExtensionInstallationDirs().get(0).listFiles();
return (files == null || files.length == 0);
}
/*
* Verifies that the installation folder is not empty and contains a folder with the given name.
*
* @param name the name of the installed extension
*/
private void checkExtensionInstalledInFilesystem(String name) {
ResourceFile[] files = appLayout.getExtensionInstallationDirs().get(0).listFiles();
assertTrue(files.length >= 1);
assertEquals(files[0].getName(), name);
}
private void checkMarkForUninstall(ExtensionDetails extension) {
checkMarkForUninstall(extension.getInstallDir());
}
private void checkMarkForUninstall(File extensionDir) {
File propertiesFile = new File(extensionDir, ExtensionUtils.PROPERTIES_FILE_NAME);
assertFalse(propertiesFile.exists());
File markedPropertiesFile =
new File(extensionDir, ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED);
assertTrue(markedPropertiesFile.exists());
}
/*
* Creates a valid extension in the archive folder. This extension is not a .zip, but a folder.
*
* @return the file representing the extension
* @throws IOException if there's an error creating the extension
*/
private File createExtensionFolderInArchiveDir() throws IOException {
ResourceFile root = new ResourceFile(appLayout.getExtensionArchiveDir(), TEST_EXT_NAME);
root.mkdir();
// Have to add a prop file so this will be recognized as an extension
File propFile = new ResourceFile(root, "extension.properties").getFile(false);
assertTrue(propFile.createNewFile());
Properties props = new Properties();
props.put("name", TEST_EXT_NAME);
props.put("description", "This is a description for " + TEST_EXT_NAME);
props.put("author", "First Last");
props.put("createdOn", new SimpleDateFormat("MM/dd/yyyy").format(new Date()));
props.put("version", Application.getApplicationVersion());
try (OutputStream os = new FileOutputStream(propFile)) {
props.store(os, null);
}
return root.getFile(false);
}
private File createExternalExtensionInFolder() throws Exception {
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
return doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME);
}
private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName)
throws Exception {
String version = Application.getApplicationVersion();
return doCreateExternalExtensionInFolder(externalFolder, extensionName, version);
}
private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName,
String version)
throws Exception {
return doCreateExternalExtensionInFolder(externalFolder, extensionName, extensionName,
version);
}
private File createExtensionWithMismatchingNamePropertyString(String nameProperty)
throws Exception {
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
String version = Application.getApplicationVersion();
return doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME, nameProperty,
version);
}
private File doCreateExternalExtensionInFolder(File externalFolder,
String extensionName, String nameProperty, String version)
throws Exception {
ResourceFile root = new ResourceFile(new ResourceFile(externalFolder), extensionName);
root.mkdir();
// Have to add a prop file so this will be recognized as an extension
File propFile = new ResourceFile(root, "extension.properties").getFile(false);
assertTrue(propFile.createNewFile());
Properties props = new Properties();
props.put("name", nameProperty);
props.put("description", "This is a description for " + extensionName);
props.put("author", "First Last");
props.put("createdOn", new SimpleDateFormat("MM/dd/yyyy").format(new Date()));
props.put("version", version);
try (OutputStream os = new FileOutputStream(propFile)) {
props.store(os, null);
}
File manifest = new ResourceFile(root, ModuleUtilities.MANIFEST_FILE_NAME).getFile(false);
manifest.createNewFile();
return root.getFile(false);
}
private List<File> createTwoExternalExtensionsInFolder() throws Exception {
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
File extension1 = doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME);
File extension2 = doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME + "Two");
return List.of(extension1, extension2);
}
/*
* Create a generic zip that is a valid extension archive.
*
* @param zipName name of the zip to create
* @return a zip file
* @throws IOException if there's an error creating the zip
*/
private File createExtensionZip(String zipName) throws IOException {
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
String version = Application.getApplicationVersion();
File f = new File(externalFolder, zipName + ".zip");
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
out.putNextEntry(new ZipEntry(zipName + "/"));
out.putNextEntry(new ZipEntry(zipName + "/extension.properties"));
StringBuilder sb = new StringBuilder();
sb.append("name:").append(zipName).append('\n');
sb.append("version:").append(version).append('\n');
byte[] data = sb.toString().getBytes();
out.write(data, 0, data.length);
out.closeEntry();
}
return f;
}
private File createExtensionZip(String zipName, String version) throws IOException {
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
File f = new File(externalFolder, zipName + ".zip");
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
out.putNextEntry(new ZipEntry(zipName + "/"));
out.putNextEntry(new ZipEntry(zipName + "/extension.properties"));
StringBuilder sb = new StringBuilder();
sb.append("name:").append(zipName).append('\n');
sb.append("version:").append(version).append('\n');
byte[] data = sb.toString().getBytes();
out.write(data, 0, data.length);
out.closeEntry();
}
return f;
}
private File createZipWithMultipleExtensions() throws IOException {
String zipName1 = "Foo";
String zipName2 = "Bar";
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
File f = new File(externalFolder, "MultiExtension.zip");
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
out.putNextEntry(new ZipEntry(zipName1 + "/"));
out.putNextEntry(new ZipEntry(zipName1 + "/extension.properties"));
out.putNextEntry(new ZipEntry(zipName2 + "/"));
out.putNextEntry(new ZipEntry(zipName2 + "/extension.properties"));
StringBuilder sb = new StringBuilder();
sb.append("name:MultiExtension");
byte[] data = sb.toString().getBytes();
out.write(data, 0, data.length);
out.closeEntry();
}
return f;
}
/*
* Create a generic zip that is NOT a valid extension archive (because it doesn't
* have an extension.properties file).
*
* @param zipName name of the zip to create
* @return a zip file
* @throws IOException if there's an error creating the zip
*/
private File createNonExtensionZip(String zipName) throws IOException {
File f = new File(appLayout.getExtensionArchiveDir().getFile(false), zipName + ".zip");
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
out.putNextEntry(new ZipEntry(zipName + "/"));
out.putNextEntry(new ZipEntry(zipName + "/randomFile.txt"));
StringBuilder sb = new StringBuilder();
sb.append("name:" + zipName);
byte[] data = sb.toString().getBytes();
out.write(data, 0, data.length);
out.closeEntry();
}
return f;
}
private class TestExtensionDetails extends ExtensionDetails {
TestExtensionDetails(String name) {
super(name, "Description", "Author", "01/01/01", "1.0");
}
}
}