GP-3569 - Cleanup of Extension management

This commit is contained in:
dragonmacher
2023-07-11 14:09:56 -04:00
parent b0e0c7372a
commit b7583dc0b9
61 changed files with 3058 additions and 2540 deletions
@@ -53,7 +53,7 @@
<P>The default tool is pre-configured with a collection of plugins relevant for the Listing and
for Debugger-related operations. As always, there is some chance that the tool will launch with
some portion of the plugins not displayed or with a less-than-optimal layout. To verify which
plugins you have, you can select <SPAN class="menu">File &rarr; Configure...</SPAN>. "Debugger"
plugins you have, you can select <SPAN class="menu">File &rarr; Configure</SPAN>. "Debugger"
should already be selected. Choosing "Configure All Plugins" (the plug icon near the top
right), should show the full list of pre-selected plugins. Debugger-related plugins all begin
with "Debugger". At a bare minimum, you will need the "DebuggerTargetsPlugin" and the
@@ -1,3 +0,0 @@
The "lib" directory is intended to hold Jar files which this contrib
is dependent upon. This directory may be eliminated from a specific
contrib if no other Jar files are needed.
@@ -1,71 +1,156 @@
<!doctype HTML public "-//W3C//DTD HTML 4.0 Frameset//EN">
<html>
<head>
<title>Extension Installation</title>
<meta http-equiv="content-type" content="text/html; charset=windows-1252">
<link rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</head>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<body>
<HTML>
<HEAD>
<TITLE>Extension Installation</TITLE>
<META http-equiv="content-type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<h1><a name="Extensions"></a>
Ghidra Extensions</h1>
<p>Ghidra Extensions (formerly 'contribs') are Ghidra software modules that are included with a Ghidra release but not
installed by default. Ghidra Extensions can be installed and uninstalled by Ghidra at runtime, with the changes taking
effect when Ghidra is restarted. This dialog can be opened by selecting the <b>Extensions</b>
option on the project file menu.</p>
<BODY>
<H1><A name="Extensions"></A> Ghidra Extensions</H1>
<p>
<center>
<table border="0" width="100%">
<tr>
<td width="100%" align="center"><img border="0" src="images/ConfigureExtensions.png"></td>
</tr>
</table>
</center>
</p>
<P>Ghidra Extensions are Ghidra software modules that can be installed
into a Ghidra distribution. This allows users to create and share new plugins and scripts.
Ghidra ships with some pre-built extensions that not installed by default.
</P>
<P>Ghidra Extensions can be installed and uninstalled at runtime, with the changes taking effect
when Ghidra is restarted. The extension installation dialog can
be opened by selecting the <B>Install Extensions</B> option on the project <B>File</B> menu.</P>
<h2>Dialog Components</h2>
<h3>Extensions List</h3>
<blockquote>
The list of extensions is populated when the dialog is launched. To build the list, Ghidra looks in several locations:
<ul>
<li>Extension Installation Directories: Contains any extensions that have been installed. The directories are located at:</li>
<ul>
<li><i>[user dir]/.ghidra/.ghidra_[version]/Extensions</i> - Installed/uninstalled from this dialog</li>
<li><i>[installation dir]/Ghidra/Extensions/</i> - Installed/uninstalled from filesystem manually</li>
</ul>
<li>Extensions Archive Directory: This is where all archive files (zips) are stored. It is located at <i>[installation dir]/Extensions/Ghidra/</i></li>
</ul>
<p><b>Note: </b> Extensions that have been installed directly into the Ghidra installation directory cannot be uninstalled
from this dialog. They must be manually removed from the filesystem.</p>
</blockquote>
<BLOCKQUOTE>
<CENTER>
<TABLE border="0" width="100%">
<TR>
<TD width="100%" align="center"><IMG alt="" border="0" src=
"images/ConfigureExtensions.png"></TD>
</TR>
</TABLE>
</CENTER>
<BR>
<BR>
</BLOCKQUOTE>
<h3>Description Panel</h3>
<blockquote>
Displays metadata about the extension selected in the Extensions List. The information displayed is extracted from the <i>extensions.properties</i> file associated
with the extension.
<H2>Dialog Components</H2>
<h4><img border="0" src="images/program_obj.png">extension.properties</h4>
<p>The existence of this file is what tells Ghidra that the folder or zip file is a Ghidra Extension. It is a simple property file that can contain the following 4 attributes:</p>
<ul>
<li><b>name</b>: Human-readable name of the extension. This is what will be displayed in the dialog.</li>
<li><b>desc</b>: Brief description of the extension.</li>
<li><b>author</b>: Creator of the extension.</li>
<li><b>createdOn</b>: Date of extension creation, in the format mm/dd/yyyy</li>
</ul>
</blockquote>
<H3>Extensions Table</H3>
<h3><a name="ExtensionTools"></a>Tools Panel</h3>
<blockquote>
<ul>
<li>&nbsp;<img border="0" src="images/Plus.png">&nbsp; Allows the user to install a new extension. An extension can be any folder or zip file that contains an <i>extensions.properties</i> file.
When one of these is selected, it will be copied to the extension installation folder and extracted (if it is a zip).
<li>&nbsp;<img border="0" src="Icons.REFRESH_ICON">&nbsp; Reloads the Extensions List
</ul>
</blockquote>
<BLOCKQUOTE>
<P>The list of extensions is populated when the dialog is launched. To build the list, Ghidra
looks in several locations:</P>
<p class="relatedtopic">Related Topics:</p>
</body>
</html>
<UL>
<LI>Extension Installation Directories: Contains any extensions that have been installed.
The directories are located at:</LI>
<LI style="list-style: none">
<UL>
<LI><I>[user dir]/.ghidra/.ghidra_[version]/Extensions</I> - Installed/uninstalled from
this dialog</LI>
<LI><I>[installation dir]/Ghidra/Extensions/</I> - Installed/uninstalled from
filesystem manually</LI>
</UL>
</LI>
<LI>Extensions Archive Directory: This is where archive files (zips) that are bundled with
the distribution are stored. It is
located at <I>[installation dir]/Extensions/Ghidra/</I>. This directory is not intended for
end-user extensions.
</LI>
</UL>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0">The color red is used in the table
to indicate that the extension version does not match the Ghidra version.</P>
</BLOCKQUOTE>
<P><B>Note:</B> Extensions that have been installed directly into the Ghidra installation
directory cannot be uninstalled from this dialog. They must be manually removed from the
filesystem.</P>
</BLOCKQUOTE>
<H3>Description Panel</H3>
<BLOCKQUOTE>
<P>Displays metadata about the extension selected in the Extensions List. The information
displayed is extracted from the <CODE><I>extensions.properties</I></CODE> file associated with the
extension.</P>
<P>The existence of this file is what tells Ghidra that the folder or zip file is a Ghidra
Extension. It is a simple property file that can contain the following attributes:</P>
<UL>
<LI><B>name</B>: Human-readable name of the extension. This is what will be displayed in
the dialog.</LI>
<LI><B>description</B>: Brief description of the extension.</LI>
<LI><B>author</B>: Creator of the extension.</LI>
<LI><B>createdOn</B>: Date of extension creation, in the format mm/dd/yyyy.</LI>
<LI><B>version</B>: The version of Ghidra for which this extension was built.</LI>
</UL>
</BLOCKQUOTE>
<H3><A name="ExtensionTools"></A>Tools Panel</H3>
<BLOCKQUOTE>
<UL>
<LI><IMG alt="" border="0" src="images/Plus.png">&nbsp; Allows the user to install a
new extension. An extension can be any folder or zip file that contains an
<I>extensions.properties</I> file. When one of these is selected, it will be copied to the
extension installation folder and extracted (if it is a zip).</LI>
<LI>&nbsp;<IMG alt="" border="0" src="Icons.REFRESH_ICON">&nbsp; Reloads the Extensions
List</LI>
</UL>
</BLOCKQUOTE>
<H2>Building Extensions</H2>
<BLOCKQUOTE>
<P>
An extension is simply a Ghidra module that contains an <CODE>extension.properties</CODE> file.
Building an extension is very similar to building a ghidra module, which is done by using
<CODE>gradle</CODE>.
</P>
<P>
Ghidra includes a <CODE>Skeleton</CODE> module in the distribution that is meant to be used as
a template when creating extensions. This module can be found at
</P>
<BLOCKQUOTE>
<P>
<CODE CLASS="path">&lt;GHIDRA_INSTALL_DIR&gt;/Extensions/Ghidra</CODE>
</P>
</BLOCKQUOTE>
<P>
Copy and rename this directory to get started writing your own module. You can then use
<CODE>gradle</CODE> to build the extension by running this command from within your extension
directory:
</P>
<BLOCKQUOTE>
<P>
<CODE CLASS="path">gradle -PGHIDRA_INSTALL_DIR=/path/to/ghidra/ghidra_&lt;version&gt;/ buildExtension</CODE>
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<BR>
<BR>
<BR>
<P class="relatedtopic">Related Topics:</P>
<UL>
<LI><A href="help/topics/Tool/Configure_Tool.htm">Configuring Tool Plugins</A></LI>
</UL>
<BR>
<BR>
<BR>
</BODY>
</HTML>
@@ -19,7 +19,7 @@
<P>This plugin doesn't perform any natural language translation by itself. The
user must install <b>string translation service</b>s that do the actual translation.
Extensions to Ghidra are installed via the <b>File
<IMG src="help/shared/arrow.gif" alt="-&gt;" border="0"> <a href="../FrontEndPlugin/Extensions.htm">Install Extensions...</a></b>
<IMG src="help/shared/arrow.gif" alt="-&gt;" border="0"> <a href="../FrontEndPlugin/Extensions.htm">Install Extensions</a></b>
menu.</P>
<P>When a string has been translated, the translated value will be shown in place of
@@ -30,8 +30,8 @@ import ghidra.framework.client.RepositoryAdapter;
import ghidra.framework.data.DomainObjectAdapter;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.model.*;
import ghidra.framework.plugintool.dialog.ExtensionUtils;
import ghidra.framework.project.DefaultProjectManager;
import ghidra.framework.project.extensions.ExtensionUtils;
import ghidra.framework.store.LockException;
import ghidra.program.database.ProgramDB;
import ghidra.util.*;
@@ -81,7 +81,7 @@ public class GhidraRun implements GhidraLaunchable {
updateSplashScreenStatusMessage("Populating Ghidra help...");
GhidraHelpService.install();
ExtensionUtils.cleanupUninstalledExtensions();
ExtensionUtils.initializeExtensions();
// Allows handling of old content which did not have a content type property
DomainObjectAdapter.setDefaultContentClass(ProgramDB.class);
@@ -26,7 +26,7 @@ import generic.jar.*;
import ghidra.GhidraApplicationLayout;
import ghidra.GhidraLaunchable;
import ghidra.framework.*;
import ghidra.framework.plugintool.dialog.ExtensionUtils;
import ghidra.framework.project.extensions.ExtensionUtils;
import ghidra.util.classfinder.ClassFinder;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.AssertException;
@@ -30,7 +30,7 @@ import ghidra.framework.model.Project;
import ghidra.framework.options.Options;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginsConfiguration;
import ghidra.framework.plugintool.PluginsConfiguration;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.model.listing.Program;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
@@ -182,9 +182,8 @@ public abstract class AbstractGhidraScriptMgrPluginTest
}
protected void deleteUserScripts() throws IOException {
Path userScriptDir = Paths.get(GhidraScriptUtil.USER_SCRIPTS_DIR);
FileUtilities.forEachFile(userScriptDir, paths -> paths.forEach(p -> delete(p)));
FileUtilities.forEachFile(userScriptDir, script -> delete(script));
}
//==================================================================================================
@@ -988,10 +987,10 @@ public abstract class AbstractGhidraScriptMgrPluginTest
// destroy any NewScriptxxx files...and Temp ones too
List<ResourceFile> paths = provider.getBundleHost()
.getBundleFiles()
.stream()
.filter(ResourceFile::isDirectory)
.collect(Collectors.toList());
.getBundleFiles()
.stream()
.filter(ResourceFile::isDirectory)
.collect(Collectors.toList());
for (ResourceFile path : paths) {
File file = path.getFile(false);
@@ -1,349 +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.util.extensions;
import static org.junit.Assert.*;
import java.io.*;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.Before;
import org.junit.Test;
import docking.test.AbstractDockingTest;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.framework.plugintool.dialog.*;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
/**
* Tests for the {@link ExtensionUtils} class.
*
*/
public class ExtensionUtilsTest extends AbstractDockingTest {
// Name used in all tests when creating extensions.
private String DEFAULT_EXT_NAME = "test";
private ApplicationLayout gLayout;
/*
* Create dummy archive and installation folders in the temp space that we can populate
* with extensions.
*/
@Before
public void setup() throws IOException {
gLayout = Application.getApplicationLayout();
// Verify that the archive and install directories are empty (each test requires
// we start with a clean slate). If they're not empty, CORRECT THE SITUATION.
if (!checkCleanInstall()) {
FileUtilities.deleteDir(gLayout.getExtensionArchiveDir().getFile(false));
for (ResourceFile installDir : gLayout.getExtensionInstallationDirs()) {
FileUtilities.deleteDir(installDir.getFile(false));
}
}
createExtensionDirs();
}
/*
* Verifies that we can install an extension from a .zip file.
*/
@Test
public void testInstallExtensionFromZip() throws IOException {
// Create an extension and install it.
ResourceFile rFile = new ResourceFile(createExtensionZip(DEFAULT_EXT_NAME));
ExtensionUtils.install(rFile);
// Verify there is something in the installation directory and it has the correct name
checkDirtyInstall(DEFAULT_EXT_NAME);
}
/*
* Verifies that we can install an extension from a folder.
*/
@Test
public void testInstallExtensionFromFolder() throws IOException {
// Create an extension and install it.
ResourceFile rFile = createExtensionFolder();
ExtensionUtils.install(rFile);
// Verify the extension is in the install folder and has the correct name
checkDirtyInstall(DEFAULT_EXT_NAME);
}
/*
* Verifies that we can uninstall an extension.
*/
@Test
public void testUninstallExtension() throws ExtensionException, IOException {
// Create an extension and install it.
ResourceFile rFile = new ResourceFile(createExtensionZip(DEFAULT_EXT_NAME));
ExtensionUtils.install(rFile);
checkDirtyInstall(DEFAULT_EXT_NAME);
// Get the extension object that we need to uninstall - there will only
// be one in the set.
Set<ExtensionDetails> extensions = ExtensionUtils.getExtensions();
assertTrue(extensions.size() == 1);
ExtensionDetails ext = extensions.iterator().next();
// Now uninstall it and verify we have a clean install folder
ExtensionUtils.uninstall(ext);
checkCleanInstall();
}
/*
* Verifies that trying to install an extension when there's already one with the same
* name installed will overwrite the existing and not throw an exception
*
* @throws Exception if there's a problem creating the temp extension folder
*/
@Test
public void testInstallExtensionDuplicate() throws Exception {
// Create an extension and install it.
ResourceFile rFile = createExtensionFolder();
ExtensionUtils.install(rFile);
// Now create another extension with the same name and try
// to install it.
rFile = new ResourceFile(createExtensionZip(DEFAULT_EXT_NAME));
boolean install = ExtensionUtils.install(rFile);
assertEquals(install, true);
}
/*
* Verifies that we can properly recognize a valid .zip file.
*/
@Test
public void testIsZip() throws IOException, ExtensionException {
File zipFile = createExtensionZip(DEFAULT_EXT_NAME);
assertTrue(ExtensionUtils.isZip(zipFile));
}
/*
* Verifies that we can identify when a .zip is a valid extension archive vs.
* just a regular old zip (ROZ).
* <p>
* Note: The presence of an extensions.properties file is the difference.
*/
@Test
public void testIsExtension_Zip() throws IOException, ExtensionException {
File zipFile1 = createExtensionZip(DEFAULT_EXT_NAME);
assertTrue(ExtensionUtils.isExtension(new ResourceFile(zipFile1)));
File zipFile2 = createNonExtensionZip(DEFAULT_EXT_NAME);
assertTrue(!ExtensionUtils.isExtension(new ResourceFile(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 IOException, ExtensionException {
File extDir = createTempDirectory("TestExtFolder");
new File(extDir, "extension.properties").createNewFile();
assertTrue(ExtensionUtils.isExtension(new ResourceFile(extDir)));
File nonExtDir = createTempDirectory("TestNonExtFolder");
assertTrue(!ExtensionUtils.isExtension(new ResourceFile(nonExtDir)));
}
/*
* Verifies that the we can retrieve all unique extensions in the archive and
* install folders.
* <p>
* Note: This test eliminates the need to test the methods for retrieving archived vs. installed
* extensions individually.
*/
@Test
public void testGetExtensions() throws ExtensionException, IOException {
// First create an extension and install it, so we have 2 extensions: one in
// the archive folder, and one in the install folder.
File zipFile = createExtensionZip(DEFAULT_EXT_NAME);
ExtensionUtils.install(new ResourceFile(zipFile));
// Now getExtensions should give us exactly 1 extension in the return.
Set<ExtensionDetails> extensions = ExtensionUtils.getExtensions();
assertTrue(extensions.size() == 1);
// Now add an archive extension with a different name and see if we get
// 2 total extensions.
createExtensionZip("Extension2");
extensions = ExtensionUtils.getExtensions();
assertTrue(extensions.size() == 2);
// Now add a 3rd extension and install it. See if we have 3 total extensions.
File extension3 = createExtensionZip("Extension3");
ExtensionUtils.install(new ResourceFile(extension3));
extensions = ExtensionUtils.getExtensions();
assertTrue(extensions.size() == 3);
}
/*
* Catch-all test for verifying that 'bad' inputs to utility functions are
* handled properly.
*/
@Test
public void testBadInputs() {
boolean foundError = false;
try {
ExtensionUtils.uninstall((ExtensionDetails) null);
ExtensionUtils.isExtension(null);
ExtensionUtils.isZip(null);
ExtensionUtils.install(new ResourceFile(new File("this/file/does/not/exist")));
ExtensionUtils.install((ResourceFile) null);
ExtensionUtils.install((ExtensionDetails) null, true);
}
catch (Exception e) {
foundError = true;
}
assertTrue(foundError == false);
}
//==================================================================================================
// Private Methods
//==================================================================================================
/*
* Creates the extension archive and installation directories.
*
* @throws IOException if there's an error creating the directories
*/
private void createExtensionDirs() throws IOException {
ResourceFile extensionDir = gLayout.getExtensionArchiveDir();
if (!extensionDir.exists()) {
if (!extensionDir.mkdir()) {
throw new IOException("Failed to create extension archive directory for test");
}
}
ResourceFile installDir = gLayout.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 = gLayout.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 checkDirtyInstall(String name) {
ResourceFile[] files = gLayout.getExtensionInstallationDirs().get(0).listFiles();
assertTrue(files.length >= 1);
assertTrue(files[0].getName().equals(name));
}
/*
* 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 ResourceFile createExtensionFolder() throws IOException {
ResourceFile root = new ResourceFile(gLayout.getExtensionArchiveDir(), DEFAULT_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);
propFile.createNewFile();
return root;
}
/*
* 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 f = new File(gLayout.getExtensionArchiveDir().getFile(false), 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:" + zipName);
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(gLayout.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;
}
}
@@ -43,7 +43,7 @@
a Code Browser by selecting the
</p>
<div class="informalexample">
<span class="bold"><strong>File -&gt; Configure...</strong></span>
<span class="bold"><strong>File -&gt; Configure</strong></span>
</div>
<p>
menu option, then clicking on the <span class="emphasis"><em>Configure</em></span> link under the
@@ -15,85 +15,30 @@
*/
package ghidra.feature.vt.api;
import static ghidra.feature.vt.db.VTTestUtils.addr;
import static ghidra.feature.vt.db.VTTestUtils.createMatchSetWithOneMatch;
import static ghidra.feature.vt.gui.util.VTOptionDefines.CALLING_CONVENTION;
import static ghidra.feature.vt.gui.util.VTOptionDefines.FUNCTION_RETURN_TYPE;
import static ghidra.feature.vt.gui.util.VTOptionDefines.FUNCTION_SIGNATURE;
import static ghidra.feature.vt.gui.util.VTOptionDefines.INLINE;
import static ghidra.feature.vt.gui.util.VTOptionDefines.NO_RETURN;
import static ghidra.feature.vt.gui.util.VTOptionDefines.PARAMETER_COMMENTS;
import static ghidra.feature.vt.gui.util.VTOptionDefines.PARAMETER_DATA_TYPES;
import static ghidra.feature.vt.gui.util.VTOptionDefines.PARAMETER_NAMES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static ghidra.feature.vt.db.VTTestUtils.*;
import static ghidra.feature.vt.gui.util.VTOptionDefines.*;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.*;
import ghidra.feature.vt.api.db.VTSessionDB;
import ghidra.feature.vt.api.main.VTAssociationStatus;
import ghidra.feature.vt.api.main.VTMarkupItem;
import ghidra.feature.vt.api.main.VTMarkupItemStatus;
import ghidra.feature.vt.api.main.VTMatch;
import ghidra.feature.vt.api.main.VTSession;
import ghidra.feature.vt.api.main.*;
import ghidra.feature.vt.api.markuptype.FunctionSignatureMarkupType;
import ghidra.feature.vt.gui.plugin.VTController;
import ghidra.feature.vt.gui.plugin.VTControllerImpl;
import ghidra.feature.vt.gui.plugin.VTPlugin;
import ghidra.feature.vt.gui.task.ApplyMatchTask;
import ghidra.feature.vt.gui.task.ClearMatchTask;
import ghidra.feature.vt.gui.task.VtTask;
import ghidra.feature.vt.gui.plugin.*;
import ghidra.feature.vt.gui.task.*;
import ghidra.feature.vt.gui.util.MatchInfo;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.CallingConventionChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.CommentChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.FunctionNameChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.FunctionSignatureChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.HighestSourcePriorityChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.LabelChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.ParameterDataTypeChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.ReplaceChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.SourcePriorityChoices;
import ghidra.feature.vt.gui.util.VTMatchApplyChoices.*;
import ghidra.feature.vt.gui.util.VTOptionDefines;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.store.LockException;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.ArrayDataType;
import ghidra.program.model.data.BooleanDataType;
import ghidra.program.model.data.CategoryPath;
import ghidra.program.model.data.CharDataType;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.FloatDataType;
import ghidra.program.model.data.IntegerDataType;
import ghidra.program.model.data.Pointer;
import ghidra.program.model.data.PointerDataType;
import ghidra.program.model.data.StructureDataType;
import ghidra.program.model.data.TypeDef;
import ghidra.program.model.data.TypedefDataType;
import ghidra.program.model.data.Undefined4DataType;
import ghidra.program.model.data.VoidDataType;
import ghidra.program.model.data.WordDataType;
import ghidra.program.model.lang.CompilerSpec;
import ghidra.program.model.lang.CompilerSpecDescription;
import ghidra.program.model.lang.CompilerSpecID;
import ghidra.program.model.lang.Language;
import ghidra.program.model.lang.LanguageID;
import ghidra.program.model.lang.LanguageNotFoundException;
import ghidra.program.model.lang.LanguageService;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.IncompatibleLanguageException;
import ghidra.program.model.listing.Parameter;
import ghidra.program.model.listing.ParameterImpl;
import ghidra.program.model.listing.Program;
import ghidra.program.model.data.*;
import ghidra.program.model.lang.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.util.DefaultLanguageService;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
@@ -104,8 +49,6 @@ import ghidra.util.task.TaskMonitor;
public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedIntegrationTest {
// private static final String TEST_SOURCE_PROGRAM_NAME = "VersionTracking/WallaceSrc";
// private static final String TEST_DESTINATION_PROGRAM_NAME = "VersionTracking/WallaceVersion2";
private TestEnv env;
private PluginTool tool;
private VTController controller;
@@ -130,8 +73,8 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
public void setUp() throws Exception {
env = new TestEnv();
sourceProgram = createSourceProgram();// env.getProgram(TEST_SOURCE_PROGRAM_NAME);
destinationProgram = createDestinationProgram();// env.getProgram(TEST_DESTINATION_PROGRAM_NAME);
sourceProgram = createSourceProgram();
destinationProgram = createDestinationProgram();
tool = env.getTool();
tool.addPlugin(VTPlugin.class.getName());
@@ -142,37 +85,15 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
sourceProgram, destinationProgram, this);
runSwing(new Runnable() {
@Override
public void run() {
controller.openVersionTrackingSession(session);
}
});
runSwing(() -> controller.openVersionTrackingSession(session));
setAllOptionsToDoNothing();
//
// env = new VTTestEnv();
// session = env.createSession(TEST_SOURCE_PROGRAM_NAME, TEST_DESTINATION_PROGRAM_NAME);
// try {
// correlator =
// vtTestEnv.correlate(new ExactMatchInstructionsProgramCorrelatorFactory(), null,
// TaskMonitor.DUMMY);
// }
// catch (Exception e) {
// Assert.fail(e.getMessage());
// e.printStackTrace();
// }
// sourceProgram = env.getSourceProgram();
// destinationProgram = env.getDestinationProgram();
// controller = env.getVTController();
// env.showTool();
//
// Logger functionLogger = Logger.getLogger(FunctionDB.class);
// functionLogger.setLevel(Level.TRACE);
//
// Configurator.setLevel(functionLogger.getName(), org.apache.logging.log4j.Level.TRACE);
//
// Logger variableLogger = Logger.getLogger(VariableSymbolDB.class);
// variableLogger.setLevel(Level.TRACE);
// Configurator.setLevel(variableLogger.getName(), org.apache.logging.log4j.Level.TRACE);
}
@@ -478,7 +399,6 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
checkSignatures("undefined use(Gadget * this, Person * person)",
"undefined FUN_00401040(void * this, undefined4 param_1)");
tx(sourceProgram, () -> {
sourceFunction.setCustomVariableStorage(true);
@@ -487,7 +407,6 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
SourceType.USER_DEFINED);
});
DataType personType = sourceProgram.getDataTypeManager().getDataType("/Person");
assertNotNull(personType);
@@ -495,7 +414,6 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
destinationFunction.setCustomVariableStorage(true);
});
// Set the function signature options for this test
ToolOptions applyOptions = controller.getOptions();
applyOptions.setEnum(FUNCTION_SIGNATURE, FunctionSignatureChoices.REPLACE);
@@ -744,24 +662,14 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
public void testApplyMatch_ReplaceSignatureAndCallingConventionDifferentLanguageFailUsingNameMatch()
throws Exception {
runSwing(new Runnable() {
@Override
public void run() {
controller.closeCurrentSessionIgnoringChanges();
}
});
runSwing(() -> controller.closeCurrentSessionIgnoringChanges());
env.release(destinationProgram);
destinationProgram = createToyDestinationProgram();// env.getProgram("helloProgram"); // get a program without cdecl
session =
VTSessionDB.createVTSession(testName.getMethodName() + " - Test Match Set Manager",
sourceProgram, destinationProgram, this);
runSwing(new Runnable() {
@Override
public void run() {
controller.openVersionTrackingSession(session);
}
});
runSwing(() -> controller.openVersionTrackingSession(session));
useMatch("0x00401040", "0x00010938");
@@ -1699,12 +1607,9 @@ public class VTMatchApplyFunctionSignatureTest extends AbstractGhidraHeadedInteg
final String[] sourceStringBox = new String[1];
final String[] destinationStringBox = new String[1];
runSwing(new Runnable() {
@Override
public void run() {
sourceStringBox[0] = sourceFunction.getPrototypeString(false, false);
destinationStringBox[0] = destinationFunction.getPrototypeString(false, false);
}
runSwing(() -> {
sourceStringBox[0] = sourceFunction.getPrototypeString(false, false);
destinationStringBox[0] = destinationFunction.getPrototypeString(false, false);
});
assertEquals(expectedSourceSignature, sourceStringBox[0]);
@@ -153,7 +153,7 @@
modified using the <A href="#Edit_Theme">Theme Editor Dialog</A>. The Theme Editor Dialog
can be invoked from the main application menu using the
<B>Edit<IMG alt="" src="help/shared/arrow.gif" border="0">Theme<IMG alt=""
src="help/shared/arrow.gif" border="0">Configure..." </b> menu. Choose the
src="help/shared/arrow.gif" border="0">Configure" </b> menu. Choose the
tab for the appropriate type and double-click on the ID column or Current Value column of the
item you want to change. An editor for that type will appear.</P>
@@ -646,11 +646,11 @@ public abstract class AbstractGenericTest extends AbstractGTest {
*/
public static void setErrorsExpected(boolean expected) {
if (expected) {
Msg.error(AbstractGenericTest.class, ">>>>>>>>>>>>>>>> Expected Exception");
Msg.error(AbstractGenericTest.class, ">>>>>>>>>>>>>>>> Expected Errors");
ConcurrentTestExceptionHandler.disable();
}
else {
Msg.error(AbstractGenericTest.class, "<<<<<<<<<<<<<<<< End Expected Exception");
Msg.error(AbstractGenericTest.class, "<<<<<<<<<<<<<<<< End Expected Errors");
ConcurrentTestExceptionHandler.enable();
}
}
@@ -76,7 +76,7 @@ public class ConcurrentTestExceptionHandler implements UncaughtExceptionHandler
* environmental issues rather than real problems. This method is intended to ignore
* these less-than-serious issues.
*
* @param throwable the throwable to examine
* @param t the throwable to examine
* @return true if it should be ignored
*/
private static boolean isKnownTestMachineTimingBug(Throwable t) {
@@ -805,7 +805,7 @@ public class Application {
*/
public static Collection<ResourceFile> getLibraryDirectories() {
checkAppInitialized();
return ModuleUtilities.getModuleLibDirectories(app.layout.getModules());
return ModuleUtilities.getModuleLibDirectories(app.layout.getModules().values());
}
/**
@@ -61,7 +61,7 @@ class ClassJar extends ClassLocation {
}
@Override
void getClasses(Set<Class<?>> set, TaskMonitor monitor) {
protected void getClasses(Set<Class<?>> set, TaskMonitor monitor) {
checkForDuplicates(set);
set.addAll(classes);
}
@@ -15,6 +15,7 @@
*/
package ghidra.util.classfinder;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
@@ -31,26 +32,35 @@ abstract class ClassLocation {
protected static final String CLASS_EXT = ".class";
final Logger log = LogManager.getLogger(getClass());
protected final Logger log = LogManager.getLogger(getClass());
protected Set<Class<?>> classes = new HashSet<>();
abstract void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException;
void checkForDuplicates(Set<Class<?>> existingClasses) {
if (!log.isTraceEnabled()) {
return;
}
protected abstract void getClasses(Set<Class<?>> set, TaskMonitor monitor)
throws CancelledException;
protected void checkForDuplicates(Set<Class<?>> existingClasses) {
for (Class<?> c : classes) {
// Note: our class and a matching class in 'existingClasses' will be '==' since the
// class loader loaded the class by name--it will always find the same class, in
// classpath order.
if (existingClasses.contains(c)) {
Module module = c.getModule();
module.toString();
log.trace("Attempting to load the same class twice: {}. " +
"Keeping loaded class ; ignoring class from {}", c, this);
return;
log.warn(() -> generateMessage(c));
}
}
}
private String generateMessage(Class<?> c) {
return String.format("Class defined in multiple locations: %s. Keeping class loaded " +
"from %s; ignoring class from %s", c, toLocation(c), this);
}
private String toLocation(Class<?> clazz) {
String name = clazz.getName();
String classAsPath = '/' + name.replace('.', '/') + ".class";
URL url = clazz.getResource(classAsPath);
String urlPath = url.getPath();
int index = urlPath.indexOf(classAsPath);
return urlPath.substring(0, index);
}
}
@@ -88,7 +88,7 @@ class ClassPackage extends ClassLocation {
}
@Override
void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException {
protected void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException {
checkForDuplicates(set);
@@ -120,4 +120,9 @@ class ClassPackage extends ClassLocation {
}
return results;
}
@Override
public String toString() {
return packageDir.toString();
}
}
@@ -44,6 +44,7 @@
<logger name="ghidra.framework" level="DEBUG"/>
<logger name="ghidra.graph" level="DEBUG" />
<!--
Turn off debug for specific project classes.
Leave ghidra.framework.project at DEBUG for tests; specific classes are higher to
@@ -52,14 +53,16 @@
<logger name="ghidra.framework.project" level="DEBUG"/>
<logger name="ghidra.framework.project.DefaultProject" level="WARN"/>
<logger name="ghidra.framework.project.DefaultProjectManager" level="INFO"/>
<logger name="functioncalls" level="DEBUG" />
<logger name="generic.random" level="WARN"/>
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>
<logger name="ghidra.net" level="WARN"/>
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
<logger name="ghidra.framework.project.extensions" level="DEBUG" />
<logger name="ghidra.framework.store.local" level="INFO"/>
<logger name="ghidra.pcodeCPort.slgh_compile" level="INFO"/>
<logger name="ghidra.pcodeCPort.slgh_compile" level="INFO"/>
<logger name="ghidra.plugins" level="INFO"/>
<logger name="ghidra.program.database" level="DEBUG" />
<logger name="ghidra.program.model.lang.xml" level="DEBUG"/>
<logger name="ghidra.app.plugin.assembler" level="DEBUG" />
@@ -73,11 +76,11 @@
<logger name="ghidra.app.util.opinion" level="DEBUG" />
<logger name="ghidra.util.classfinder" level="DEBUG" />
<logger name="ghidra.util.task" level="DEBUG" />
<logger name="org.jungrapht.visualization" level="WARN" />
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />
<logger name="org.jungrapht.visualization" level="WARN" />
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />
<Root level="ALL">
<AppenderRef ref="console" level="DEBUG"/>
<AppenderRef ref="console" level="DEBUG"/>
<AppenderRef ref="detail" level="DEBUG"/>
<AppenderRef ref="script" level="DEBUG"/>
<AppenderRef ref="logPanel" level="INFO"/>
@@ -57,7 +57,8 @@
<logger name="generic.random" level="WARN"/>
<logger name="ghidra.app.plugin.core.progmgr.ProgramManagerPlugin" level="WARN"/>
<logger name="ghidra.net" level="WARN"/>
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
<logger name="ghidra.app.plugin.core.misc.RecoverySnapshotMgrPlugin" level="INFO"/>
<logger name="ghidra.framework.project.extensions" level="DEBUG" />
<logger name="ghidra.framework.store.local" level="INFO"/>
<logger name="ghidra.pcodeCPort.slgh_compile" level="INFO"/>
<logger name="ghidra.plugins" level="INFO"/>
@@ -73,7 +74,10 @@
<logger name="ghidra.app.util.importer" level="INFO" />
<logger name="ghidra.app.util.opinion" level="DEBUG" />
<logger name="ghidra.util.classfinder" level="DEBUG" />
<logger name="ghidra.util.task" level="DEBUG" />
<logger name="ghidra.util.task" level="DEBUG" />
<logger name="org.jungrapht.visualization" level="WARN" />
<logger name="org.jungrapht.visualization.DefaultVisualizationServer" level="DEBUG" />
<Root level="ALL">
<AppenderRef ref="console" level="TRACE"/>
@@ -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;
}
}
@@ -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,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
@@ -55,7 +55,7 @@ public class SpecExtensionPanel extends JPanel {
private boolean unappliedChanges;
private SpecExtension specExtension;
private List<CompilerElement> tableElements;
private ExtensionTableModel tableModel;
private SpecExtensionTableModel tableModel;
private GTable extensionTable;
private JButton exportButton;
private JButton removeButton;
@@ -163,7 +163,7 @@ public class SpecExtensionPanel extends JPanel {
}
}
private class ExtensionTableModel extends AbstractGTableModel<CompilerElement> {
private class SpecExtensionTableModel extends AbstractGTableModel<CompilerElement> {
private final String[] columnNames = { "Extension Type", "Name", "Status" };
@Override
@@ -383,7 +383,7 @@ public class SpecExtensionPanel extends JPanel {
private void createPanel() {
setLayout(new BorderLayout(10, 10));
tableModel = new ExtensionTableModel();
tableModel = new SpecExtensionTableModel();
extensionTable = new CompilerElementTable(tableModel);
JScrollPane sp = new JScrollPane(extensionTable);

Some files were not shown because too many files have changed in this diff Show More