mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 19:42:36 +02:00
GP-3569 - Cleanup of Extension management
This commit is contained in:
parent
b0e0c7372a
commit
b7583dc0b9
61 changed files with 3058 additions and 2540 deletions
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()));
|
||||
}
|
||||
|
|
@ -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() {
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue