mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-24 21:21:56 +00:00
GP-3569 - Cleanup of Extension management
This commit is contained in:
parent
b0e0c7372a
commit
b7583dc0b9
@ -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 → Configure...</SPAN>. "Debugger"
|
||||
plugins you have, you can select <SPAN class="menu">File → 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> <img border="0" src="images/Plus.png"> 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> <img border="0" src="Icons.REFRESH_ICON"> 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"> 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> <IMG alt="" border="0" src="Icons.REFRESH_ICON"> 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"><GHIDRA_INSTALL_DIR>/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_<version>/ 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="->" border="0"> <a href="../FrontEndPlugin/Extensions.htm">Install Extensions...</a></b>
|
||||
<IMG src="help/shared/arrow.gif" alt="->" 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 -> Configure...</strong></span>
|
||||
<span class="bold"><strong>File -> 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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package ghidra.framework.plugintool.dialog;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -93,6 +94,8 @@ class PluginInstallerTableModel
|
||||
descriptor.addVisibleColumn(new PluginNameColumn(), 1, true);
|
||||
descriptor.addVisibleColumn(new PluginDescriptionColumn());
|
||||
descriptor.addVisibleColumn(new PluginCategoryColumn());
|
||||
descriptor.addHiddenColumn(new PluginModuleColumn());
|
||||
descriptor.addHiddenColumn(new PluginLocationColumn());
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
@ -177,7 +180,7 @@ class PluginInstallerTableModel
|
||||
* Column for displaying the interactive checkbox, allowing the user to install
|
||||
* or uninstall the plugin.
|
||||
*/
|
||||
class PluginInstalledColumn extends
|
||||
private class PluginInstalledColumn extends
|
||||
AbstractDynamicTableColumn<PluginDescription, Boolean, List<PluginDescription>> {
|
||||
|
||||
@Override
|
||||
@ -200,7 +203,7 @@ class PluginInstallerTableModel
|
||||
/**
|
||||
* Column for displaying the status of the plugin.
|
||||
*/
|
||||
class PluginStatusColumn
|
||||
private class PluginStatusColumn
|
||||
extends AbstractDynamicTableColumn<PluginDescription, Icon, List<PluginDescription>> {
|
||||
|
||||
@Override
|
||||
@ -223,7 +226,7 @@ class PluginInstallerTableModel
|
||||
/**
|
||||
* Column for displaying the extension name of the plugin.
|
||||
*/
|
||||
class PluginNameColumn
|
||||
private class PluginNameColumn
|
||||
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
|
||||
|
||||
@Override
|
||||
@ -246,7 +249,7 @@ class PluginInstallerTableModel
|
||||
/**
|
||||
* Column for displaying the plugin description.
|
||||
*/
|
||||
class PluginDescriptionColumn
|
||||
private class PluginDescriptionColumn
|
||||
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
|
||||
|
||||
@Override
|
||||
@ -266,10 +269,54 @@ class PluginInstallerTableModel
|
||||
}
|
||||
}
|
||||
|
||||
private class PluginModuleColumn
|
||||
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Module";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(PluginDescription rowObject, Settings settings,
|
||||
List<PluginDescription> data, ServiceProvider sp) throws IllegalArgumentException {
|
||||
return rowObject.getModuleName();
|
||||
}
|
||||
}
|
||||
|
||||
private class PluginLocationColumn
|
||||
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Location";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnPreferredWidth() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue(PluginDescription rowObject, Settings settings,
|
||||
List<PluginDescription> data, ServiceProvider sp) throws IllegalArgumentException {
|
||||
Class<? extends Plugin> clazz = rowObject.getPluginClass();
|
||||
String name = clazz.getName();
|
||||
String path = '/' + name.replace('.', '/') + ".class";
|
||||
URL url = clazz.getResource(path);
|
||||
return url.getFile();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Column for displaying the plugin category.
|
||||
*/
|
||||
class PluginCategoryColumn
|
||||
private class PluginCategoryColumn
|
||||
extends AbstractDynamicTableColumn<PluginDescription, String, List<PluginDescription>> {
|
||||
|
||||
@Override
|
||||
|
@ -16,6 +16,7 @@
|
||||
package ghidra.framework.plugintool.util;
|
||||
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.framework.plugintool.PluginsConfiguration;
|
||||
|
||||
/**
|
||||
* A configuration that includes all plugins on the classpath.
|
||||
|
@ -148,14 +148,13 @@ public class PluginDescription implements Comparable<PluginDescription> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type for the plugin: CORE, CONTRIB, PROTOTYPE, or
|
||||
* DEVELOP. Within a type, plugins are grouped by category.
|
||||
* @return the type (or null if there is no module)
|
||||
* Return the name of the module that contains the plugin.
|
||||
* @return the module name
|
||||
*/
|
||||
public String getModuleName() {
|
||||
if (moduleName == null) {
|
||||
ResourceFile moduleRootDirectory = Application.getMyModuleRootDirectory();
|
||||
moduleName = (moduleRootDirectory == null) ? null : moduleRootDirectory.getName();
|
||||
ResourceFile moduleDir = Application.getModuleContainingClass(pluginClass.getName());
|
||||
moduleName = (moduleDir == null) ? "<No Module>" : moduleDir.getName();
|
||||
}
|
||||
|
||||
return moduleName;
|
||||
|
@ -15,14 +15,9 @@
|
||||
*/
|
||||
package ghidra.framework.plugintool.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.dialog.ExtensionDetails;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.exception.AssertException;
|
||||
@ -33,99 +28,6 @@ import ghidra.util.exception.AssertException;
|
||||
*/
|
||||
public class PluginUtils {
|
||||
|
||||
/**
|
||||
* Finds all plugin classes loaded from a given set of extensions.
|
||||
*
|
||||
* @param extensions set of extensions to search
|
||||
* @return list of loaded plugin classes, or empty list if none found
|
||||
*/
|
||||
public static List<Class<?>> findLoadedPlugins(Set<ExtensionDetails> extensions) {
|
||||
|
||||
List<Class<?>> pluginClasses = new ArrayList<>();
|
||||
for (ExtensionDetails extension : extensions) {
|
||||
|
||||
if (extension == null || extension.getInstallPath() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Class<?>> classes = findLoadedPlugins(new File(extension.getInstallPath()));
|
||||
pluginClasses.addAll(classes);
|
||||
}
|
||||
|
||||
return pluginClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all plugin classes loaded from a particular folder/file.
|
||||
* <p>
|
||||
* This uses the {@link ClassSearcher} to find all <code>Plugin.class</code> objects on the
|
||||
* classpath. For each class, the original resource file is compared against the
|
||||
* given folder and if it's contained therein (or if it matches a given jar), it's
|
||||
* added to the return list.
|
||||
*
|
||||
* @param dir the directory to search, or a jar file
|
||||
* @return list of {@link Plugin} classes, or empty list if none found
|
||||
*/
|
||||
private static List<Class<?>> findLoadedPlugins(File dir) {
|
||||
|
||||
// The list of classes to return.
|
||||
List<Class<?>> retPlugins = new ArrayList<>();
|
||||
|
||||
// Find any jar files in the directory provided. Our plugin(s) will always be
|
||||
// in a jar.
|
||||
List<File> jarFiles = new ArrayList<>();
|
||||
findJarFiles(dir, jarFiles);
|
||||
|
||||
// Now get all Plugin.class files that have been loaded, and see if any of them
|
||||
// were loaded from one of the jars we just found.
|
||||
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
|
||||
for (Class<? extends Plugin> plugin : plugins) {
|
||||
URL location = plugin.getResource('/' + plugin.getName().replace('.', '/') + ".class");
|
||||
if (location == null) {
|
||||
Msg.warn(null, "Class location for plugin [" + plugin.getName() +
|
||||
"] could not be determined.");
|
||||
continue;
|
||||
}
|
||||
String pluginLocation = location.getPath();
|
||||
for (File jar : jarFiles) {
|
||||
URL jarUrl = null;
|
||||
try {
|
||||
jarUrl = jar.toURI().toURL();
|
||||
if (pluginLocation.contains(jarUrl.getPath())) {
|
||||
retPlugins.add(plugin);
|
||||
}
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return retPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the given list with all discovered jar files found in the given directory and
|
||||
* its subdirectories.
|
||||
*
|
||||
* @param dir the directory to search
|
||||
* @param jarFiles list of found jar files
|
||||
*/
|
||||
private static void findJarFiles(File dir, List<File> jarFiles) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
findJarFiles(f, jarFiles);
|
||||
}
|
||||
|
||||
if (f.isFile() && f.getName().endsWith(".jar")) {
|
||||
jarFiles.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of a {@link Plugin}.
|
||||
*
|
||||
@ -268,37 +170,4 @@ public class PluginUtils {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the specified Plugin class is well-formed and meets requirements for
|
||||
* Ghidra Plugins:
|
||||
* <ul>
|
||||
* <li>Has a constructor with a signature of <code>ThePlugin(PluginTool tool)</code>
|
||||
* <li>Has a {@link PluginInfo @PluginInfo} annotation.
|
||||
* </ul>
|
||||
* <p>
|
||||
* See {@link Plugin}.
|
||||
* <p>
|
||||
* @param pluginClass Class to examine.
|
||||
* @return boolean true if well formed.
|
||||
*/
|
||||
public static boolean isValidPluginClass(Class<? extends Plugin> pluginClass) {
|
||||
try {
|
||||
// will throw exception if missing ctor
|
||||
pluginClass.getConstructor(PluginTool.class);
|
||||
|
||||
// #if ( can_do_strict_checking )
|
||||
// PluginInfo pia = pluginClass.getAnnotation(PluginInfo.class);
|
||||
// return pia != null;
|
||||
// #else
|
||||
// for now
|
||||
return true;
|
||||
// #endif
|
||||
}
|
||||
catch (NoSuchMethodException e) {
|
||||
// no matching constructor method
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,366 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.project.extensions;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import generic.json.Json;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Msg;
|
||||
import utilities.util.FileUtilities;
|
||||
import utility.application.ApplicationLayout;
|
||||
import utility.module.ModuleUtilities;
|
||||
|
||||
/**
|
||||
* Representation of a Ghidra extension. This class encapsulates all information required to
|
||||
* uniquely identify an extension and where (or if) it has been installed.
|
||||
* <p>
|
||||
* Note that hashCode and equals have been implemented for this. Two extension
|
||||
* descriptions are considered equal if they have the same {@link #name} attribute; all other
|
||||
* fields are unimportant except for display purposes.
|
||||
*/
|
||||
public class ExtensionDetails implements Comparable<ExtensionDetails> {
|
||||
|
||||
/** Absolute path to where this extension is installed. If not installed, this will be null. */
|
||||
private File installDir;
|
||||
|
||||
/**
|
||||
* Absolute path to where the original source archive (zip) for this extension can be found. If
|
||||
* there is no archive (likely because this is an extension that comes pre-installed with
|
||||
* Ghidra, or Ghidra is being run in development mode), this will be null.
|
||||
*/
|
||||
private String archivePath;
|
||||
|
||||
/** Name of the extension. This must be unique.*/
|
||||
private String name;
|
||||
|
||||
/** Brief description, for display purposes only.*/
|
||||
private String description;
|
||||
|
||||
/** Date when the extension was created, for display purposes only.*/
|
||||
private String createdOn;
|
||||
|
||||
/** Author of the extension, for display purposes only.*/
|
||||
private String author;
|
||||
|
||||
/** The extension version */
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param name unique name of the extension; cannot be null
|
||||
* @param description brief explanation of what the extension does; can be null
|
||||
* @param author creator of the extension; can be null
|
||||
* @param createdOn creation date of the extension, can be null
|
||||
* @param version the extension version
|
||||
*/
|
||||
public ExtensionDetails(String name, String description, String author, String createdOn,
|
||||
String version) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.author = author;
|
||||
this.createdOn = createdOn;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ExtensionDetails other = (ExtensionDetails) obj;
|
||||
if (name == null) {
|
||||
if (other.name != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!name.equals(other.name)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location where this extension is installed. If the extension is not installed
|
||||
* this will be null.
|
||||
*
|
||||
* @return the extension path, or null
|
||||
*/
|
||||
public String getInstallPath() {
|
||||
if (installDir != null) {
|
||||
return installDir.getAbsolutePath();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public File getInstallDir() {
|
||||
return installDir;
|
||||
}
|
||||
|
||||
public void setInstallDir(File installDir) {
|
||||
this.installDir = installDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the location where the extension archive is located. The extension archive concept
|
||||
* is not used for all extensions, but is used for delivering extensions as part of a
|
||||
* distribution.
|
||||
*
|
||||
* @return the archive path, or null
|
||||
* @see ApplicationLayout#getExtensionArchiveDir()
|
||||
*/
|
||||
public String getArchivePath() {
|
||||
return archivePath;
|
||||
}
|
||||
|
||||
public void setArchivePath(String path) {
|
||||
this.archivePath = path;
|
||||
}
|
||||
|
||||
public boolean isFromArchive() {
|
||||
return archivePath != null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getCreatedOn() {
|
||||
return createdOn;
|
||||
}
|
||||
|
||||
public void setCreatedOn(String date) {
|
||||
this.createdOn = date;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
/**
|
||||
* An extension is known to be installed if it has a valid installation path AND that path
|
||||
* contains a Module.manifest file. Extensions that are {@link #isPendingUninstall()} are
|
||||
* still on the filesystem, may be in use by the tool, but will be removed upon restart.
|
||||
* <p>
|
||||
* Note: The module manifest file is a marker that indicates several things; one of which is
|
||||
* the installation status of an extension. When a user marks an extension to be uninstalled (by
|
||||
* checking the appropriate checkbox in the {@link ExtensionTableModel}), the only thing
|
||||
* that is done is to remove this manifest file, which tells the {@link ExtensionTableProvider}
|
||||
* to remove the entire extension directory on the next launch.
|
||||
*
|
||||
* @return true if the extension is installed.
|
||||
*/
|
||||
public boolean isInstalled() {
|
||||
if (installDir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If running out of a jar and the install path is valid, just return true. The alternative
|
||||
// would be to inspect the jar and verify that the install path is there and is valid, but
|
||||
// that's overkill.
|
||||
if (Application.inSingleJarMode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
File f = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME);
|
||||
return f.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this extension is marked to be uninstalled. The contents of the extension
|
||||
* still exist and the tool may still be using the extension, but on restart, the extension will
|
||||
* be removed.
|
||||
*
|
||||
* @return true if marked for uninstall
|
||||
*/
|
||||
public boolean isPendingUninstall() {
|
||||
if (installDir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Application.inSingleJarMode()) {
|
||||
return false; // can't uninstall from single jar mode
|
||||
}
|
||||
|
||||
File f = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED);
|
||||
return f.exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this extension is installed under an installation folder or inside of a
|
||||
* source control repository folder.
|
||||
* @return true if this extension is installed under an installation folder or inside of a
|
||||
* source control repository folder.
|
||||
*/
|
||||
public boolean isInstalledInInstallationFolder() {
|
||||
if (installDir == null) {
|
||||
return false; // not installed
|
||||
}
|
||||
|
||||
ApplicationLayout layout = Application.getApplicationLayout();
|
||||
File appInstallDir = layout.getApplicationInstallationDir().getFile(false);
|
||||
if (FileUtilities.isPathContainedWithin(appInstallDir, installDir)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the module manifest and extension properties file that are in an installed state to
|
||||
* an uninstalled state.
|
||||
*
|
||||
* Specifically, the following will be renamed:
|
||||
* <UL>
|
||||
* <LI>Module.manifest to Module.manifest.uninstalled</LI>
|
||||
* <LI>extension.properties = extension.properties.uninstalled</LI>
|
||||
* </UL>
|
||||
*
|
||||
* @return false if any renames fail
|
||||
*/
|
||||
public boolean markForUninstall() {
|
||||
|
||||
if (installDir == null) {
|
||||
return false; // already marked as uninstalled
|
||||
}
|
||||
|
||||
Msg.trace(this, "Marking extension for uninstall '" + installDir + "'");
|
||||
|
||||
boolean success = true;
|
||||
File manifest = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME);
|
||||
if (manifest.exists()) {
|
||||
File newFile = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED);
|
||||
if (!manifest.renameTo(newFile)) {
|
||||
Msg.trace(this, "Unable to rename module manifest file: " + manifest);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Msg.trace(this, "No manifest file found for extension '" + name + "'");
|
||||
}
|
||||
|
||||
File properties = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME);
|
||||
if (properties.exists()) {
|
||||
File newFile = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED);
|
||||
if (!properties.renameTo(newFile)) {
|
||||
Msg.trace(this, "Unable to rename properties file: " + properties);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Msg.trace(this, "No properties file found for extension '" + name + "'");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* A companion method for {@link #markForUninstall()} that allows extensions marked for cleanup
|
||||
* to be restored to the installed state.
|
||||
* <p>
|
||||
* Specifically, the following will be renamed:
|
||||
* <UL>
|
||||
* <LI>Module.manifest.uninstalled to Module.manifest</LI>
|
||||
* <LI>extension.properties.uninstalled to extension.properties</LI>
|
||||
* </UL>
|
||||
* @return true if successful
|
||||
*/
|
||||
public boolean clearMarkForUninstall() {
|
||||
|
||||
if (installDir == null) {
|
||||
Msg.error(ExtensionUtils.class,
|
||||
"Cannot restore extension; extension installation dir is missing for: " + name);
|
||||
return false; // already marked as uninstalled
|
||||
}
|
||||
|
||||
Msg.trace(this, "Restoring extension state files for '" + installDir + "'");
|
||||
|
||||
boolean success = true;
|
||||
File manifest = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED);
|
||||
if (manifest.exists()) {
|
||||
File newFile = new File(installDir, ModuleUtilities.MANIFEST_FILE_NAME);
|
||||
if (!manifest.renameTo(newFile)) {
|
||||
Msg.trace(this, "Unable to rename module manifest file: " + manifest);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Msg.trace(this, "No manifest file found for extension '" + name + "'");
|
||||
}
|
||||
|
||||
File properties = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED);
|
||||
if (properties.exists()) {
|
||||
File newFile = new File(installDir, ExtensionUtils.PROPERTIES_FILE_NAME);
|
||||
if (!properties.renameTo(newFile)) {
|
||||
Msg.trace(this, "Unable to rename properties file: " + properties);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Msg.trace(this, "No properties file found for extension '" + name + "'");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ExtensionDetails other) {
|
||||
return name.compareTo(other.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Json.toString(this);
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.plugintool.dialog;
|
||||
package ghidra.framework.project.extensions;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Point;
|
||||
@ -22,6 +22,7 @@ import javax.swing.text.SimpleAttributeSet;
|
||||
|
||||
import docking.widgets.table.threaded.ThreadedTableModelListener;
|
||||
import generic.theme.GColor;
|
||||
import ghidra.framework.plugintool.dialog.AbstractDetailsPanel;
|
||||
|
||||
/**
|
||||
* Panel that shows information about the selected extension in the {@link ExtensionTablePanel}. This
|
@ -13,10 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.plugintool.dialog;
|
||||
package ghidra.framework.project.extensions;
|
||||
|
||||
import java.awt.Component;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
import docking.widgets.table.*;
|
||||
@ -26,13 +25,11 @@ import ghidra.docking.settings.Settings;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.datastruct.Accumulator;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.table.column.AbstractGColumnRenderer;
|
||||
import ghidra.util.table.column.GColumnRenderer;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
* Model for the {@link ExtensionTablePanel}. This defines 5 columns for displaying information in
|
||||
@ -59,9 +56,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
|
||||
/** This is the data source for the model. Whatever is here will be displayed in the table. */
|
||||
private Set<ExtensionDetails> extensions;
|
||||
|
||||
/** Indicates if the model has changed due to an install or uninstall. */
|
||||
private boolean modelChanged = false;
|
||||
private Map<String, Boolean> originalInstallStates = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@ -94,21 +89,17 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
if (Application.inSingleJarMode() || SystemUtilities.isInDevelopmentMode()) {
|
||||
if (Application.inSingleJarMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not allow GUI removal of extensions manually installed in installation directory.
|
||||
ExtensionDetails extension = getSelectedExtension(rowIndex);
|
||||
|
||||
// Do not allow GUI uninstallation of extensions manually installed in installation
|
||||
// directory
|
||||
if (extension.getInstallPath() != null && FileUtilities.isPathContainedWithin(
|
||||
Application.getApplicationLayout().getApplicationInstallationDir().getFile(false),
|
||||
new File(extension.getInstallPath()))) {
|
||||
if (extension.isInstalledInInstallationFolder()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (columnIndex == INSTALLED_COL);
|
||||
return columnIndex == INSTALLED_COL;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -117,7 +108,6 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
*/
|
||||
@Override
|
||||
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||
super.setValueAt(aValue, rowIndex, columnIndex);
|
||||
|
||||
// We only care about the install column here, as it's the only one that
|
||||
// is editable.
|
||||
@ -131,31 +121,50 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
Application.getApplicationLayout().getExtensionInstallationDirs().get(0);
|
||||
if (!installDir.exists() && !installDir.mkdir()) {
|
||||
Msg.showError(this, null, "Directory Error",
|
||||
"Cannot install/uninstall extensions: Failed to create extension installation directory.\n" +
|
||||
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
|
||||
"Cannot install/uninstall extensions: Failed to create extension installation " +
|
||||
"directory.\nSee the \"Ghidra Extension Notes\" section of the Ghidra " +
|
||||
"Installation Guide for more information.");
|
||||
}
|
||||
if (!installDir.canWrite()) {
|
||||
Msg.showError(this, null, "Permissions Error",
|
||||
"Cannot install/uninstall extensions: Invalid write permissions on installation directory.\n" +
|
||||
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
|
||||
"Cannot install/uninstall extensions: Invalid write permissions on installation " +
|
||||
"directory.\nSee the \"Ghidra Extension Notes\" section of the Ghidra " +
|
||||
"Installation Guide for more information.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean install = ((Boolean) aValue).booleanValue();
|
||||
ExtensionDetails extension = getSelectedExtension(rowIndex);
|
||||
|
||||
if (install) {
|
||||
if (ExtensionUtils.install(extension, true)) {
|
||||
modelChanged = true;
|
||||
if (!install) {
|
||||
if (extension.markForUninstall()) {
|
||||
refreshTable();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (ExtensionUtils.removeStateFiles(extension)) {
|
||||
modelChanged = true;
|
||||
|
||||
// Restore an existing extension or install an archived extension
|
||||
if (extension.isPendingUninstall()) {
|
||||
if (extension.clearMarkForUninstall()) {
|
||||
refreshTable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
refreshTable();
|
||||
// At this point, the extension is not installed, so we cannot simply clear the uninstall
|
||||
// state. This means that the extension has not yet been installed. The only way to get
|
||||
// into this state is by clicking an extension that was discovered in the 'extension
|
||||
// archives folder'
|
||||
if (extension.isFromArchive()) {
|
||||
if (ExtensionUtils.installExtensionFromArchive(extension)) {
|
||||
refreshTable();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a programming error
|
||||
Msg.error(this,
|
||||
"Unable install an extension that no longer exists. Restart Ghidra and " +
|
||||
"try manually installing the extension: '" + extension.getName() + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,10 +173,9 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
* @param details the extension to check
|
||||
* @return true if extension version is valid for this version of Ghidra
|
||||
*/
|
||||
private boolean isValidVersion(ExtensionDetails details) {
|
||||
private boolean matchesGhidraVersion(ExtensionDetails details) {
|
||||
String ghidraVersion = Application.getApplicationVersion();
|
||||
String extensionVersion = details.getVersion();
|
||||
|
||||
return ghidraVersion.equals(extensionVersion);
|
||||
}
|
||||
|
||||
@ -184,12 +192,28 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
extensions = ExtensionUtils.getExtensions();
|
||||
Set<ExtensionDetails> archived = ExtensionUtils.getArchiveExtensions();
|
||||
Set<ExtensionDetails> installed = ExtensionUtils.getInstalledExtensions();
|
||||
|
||||
// don't show archived extensions that have been installed
|
||||
for (ExtensionDetails extension : installed) {
|
||||
if (archived.remove(extension)) {
|
||||
Msg.trace(this,
|
||||
"Not showing archived extension that has been installed. Archive path: " +
|
||||
extension.getArchivePath()); // useful for debugging
|
||||
}
|
||||
}
|
||||
catch (ExtensionException e) {
|
||||
Msg.error(this, "Error loading extensions", e);
|
||||
return;
|
||||
|
||||
extensions = new HashSet<>();
|
||||
extensions.addAll(installed);
|
||||
extensions.addAll(archived);
|
||||
|
||||
for (ExtensionDetails e : extensions) {
|
||||
String name = e.getName();
|
||||
if (originalInstallStates.containsKey(name)) {
|
||||
continue; // preserve the original value
|
||||
}
|
||||
originalInstallStates.put(e.getName(), e.isInstalled());
|
||||
}
|
||||
|
||||
accumulator.addAll(extensions);
|
||||
@ -201,7 +225,15 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
* @return true if the model has changed as a result of installing or uninstalling an extension
|
||||
*/
|
||||
public boolean hasModelChanged() {
|
||||
return modelChanged;
|
||||
|
||||
for (ExtensionDetails e : extensions) {
|
||||
Boolean wasInstalled = originalInstallStates.get(e.getName());
|
||||
if (e.isInstalled() != wasInstalled) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,7 +273,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
private class ExtensionNameColumn
|
||||
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
|
||||
|
||||
private ExtVersionRenderer renderer = new ExtVersionRenderer();
|
||||
private ExtRenderer renderer = new ExtRenderer();
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
@ -271,7 +303,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
private class ExtensionDescriptionColumn
|
||||
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
|
||||
|
||||
private ExtVersionRenderer renderer = new ExtVersionRenderer();
|
||||
private ExtRenderer renderer = new ExtRenderer();
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
@ -301,7 +333,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
private class ExtensionVersionColumn
|
||||
extends AbstractDynamicTableColumn<ExtensionDetails, String, Object> {
|
||||
|
||||
private ExtVersionRenderer renderer = new ExtVersionRenderer();
|
||||
private ExtRenderer renderer = new ExtRenderer();
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
@ -403,14 +435,14 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
}
|
||||
}
|
||||
|
||||
private class ExtVersionRenderer extends AbstractGColumnRenderer<String> {
|
||||
private class ExtRenderer extends AbstractGColumnRenderer<String> {
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
|
||||
Component comp = super.getTableCellRendererComponent(data);
|
||||
|
||||
ExtensionDetails extension = getSelectedExtension(data.getRowViewIndex());
|
||||
if (!isValidVersion(extension)) {
|
||||
if (!matchesGhidraVersion(extension)) {
|
||||
comp.setForeground(getErrorForegroundColor(data.isSelected()));
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.plugintool.dialog;
|
||||
package ghidra.framework.project.extensions;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
@ -70,9 +70,6 @@ public class ExtensionTablePanel extends JPanel {
|
||||
// way to restrict column width.
|
||||
TableColumn col = table.getColumnModel().getColumn(ExtensionTableModel.INSTALLED_COL);
|
||||
col.setMaxWidth(25);
|
||||
|
||||
// Finally, load the table with some data.
|
||||
refreshTable();
|
||||
}
|
||||
|
||||
public void dispose() {
|
@ -13,12 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.plugintool.dialog;
|
||||
package ghidra.framework.project.extensions;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
@ -32,7 +31,8 @@ import generic.jar.ResourceFile;
|
||||
import ghidra.app.util.GenericHelpTopics;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.filechooser.GhidraFileChooserModel;
|
||||
import ghidra.util.filechooser.GhidraFileFilter;
|
||||
import resources.Icons;
|
||||
@ -43,6 +43,8 @@ import resources.Icons;
|
||||
*/
|
||||
public class ExtensionTableProvider extends DialogComponentProvider {
|
||||
|
||||
private static final String LAST_IMPORT_DIRECTORY_KEY = "LastExtensionImportDirectory";
|
||||
|
||||
private ExtensionTablePanel extensionTablePanel;
|
||||
|
||||
private boolean requireRestart = false;
|
||||
@ -126,57 +128,28 @@ public class ExtensionTableProvider extends DialogComponentProvider {
|
||||
Application.getApplicationLayout().getExtensionInstallationDirs().get(0);
|
||||
if (!installDir.exists() && !installDir.mkdir()) {
|
||||
Msg.showError(this, null, "Directory Error",
|
||||
"Cannot install/uninstall extensions: Failed to create extension installation directory.\n" +
|
||||
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
|
||||
"Cannot install/uninstall extensions: Failed to create extension " +
|
||||
"installation directory: " + installDir);
|
||||
}
|
||||
if (!installDir.canWrite()) {
|
||||
Msg.showError(this, null, "Permissions Error",
|
||||
"Cannot install/uninstall extensions: Invalid write permissions on installation directory.\n" +
|
||||
"See the \"Ghidra Extension Notes\" section of the Ghidra Installation Guide for more information.");
|
||||
"Cannot install/uninstall extensions: Invalid write permissions on " +
|
||||
"installation directory: " + installDir);
|
||||
return;
|
||||
}
|
||||
|
||||
GhidraFileChooser chooser = new GhidraFileChooser(getComponent());
|
||||
chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_AND_DIRECTORIES);
|
||||
chooser.setTitle("Select extension");
|
||||
chooser.setLastDirectoryPreference(LAST_IMPORT_DIRECTORY_KEY);
|
||||
chooser.setTitle("Select Extension");
|
||||
chooser.addFileFilter(new ExtensionFileFilter());
|
||||
|
||||
List<File> files = chooser.getSelectedFiles();
|
||||
chooser.dispose();
|
||||
for (File file : files) {
|
||||
try {
|
||||
if (!ExtensionUtils.isExtension(new ResourceFile(file))) {
|
||||
Msg.showError(this, null, "Installation Error", "Selected file: [" +
|
||||
file.getName() + "] is not a valid Ghidra Extension");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (ExtensionException e1) {
|
||||
Msg.showError(this, null, "Installation Error", "Error determining if [" +
|
||||
file.getName() + "] is a valid Ghidra Extension", e1);
|
||||
continue;
|
||||
}
|
||||
|
||||
String extensionVersion = getExtensionVersion(file);
|
||||
if (extensionVersion == null) {
|
||||
Msg.showError(this, null, "Installation Error",
|
||||
"Unable to read extension version for [" + file + "]");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ExtensionUtils.validateExtensionVersion(extensionVersion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (ExtensionUtils.install(new ResourceFile(file))) {
|
||||
panel.refreshTable();
|
||||
requireRestart = true;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(null, "Problem installing extension [" + file.getName() + "]", e);
|
||||
}
|
||||
if (installExtensions(files)) {
|
||||
panel.refreshTable();
|
||||
requireRestart = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -185,49 +158,18 @@ public class ExtensionTableProvider extends DialogComponentProvider {
|
||||
addAction.setMenuBarData(new MenuData(new String[] { "Add Extension" }, addIcon, group));
|
||||
addAction.setToolBarData(new ToolBarData(addIcon, group));
|
||||
addAction.setHelpLocation(new HelpLocation(GenericHelpTopics.FRONT_END, "ExtensionTools"));
|
||||
addAction.setDescription(
|
||||
SystemUtilities.isInDevelopmentMode() ? "Add Extension (disabled in development mode)"
|
||||
: "Add extension");
|
||||
addAction.setEnabled(
|
||||
!SystemUtilities.isInDevelopmentMode() && !Application.inSingleJarMode());
|
||||
addAction.setDescription("Add extension");
|
||||
addAction.setEnabled(!Application.inSingleJarMode());
|
||||
addAction(addAction);
|
||||
}
|
||||
|
||||
private String getExtensionVersion(File file) {
|
||||
|
||||
// If the given file is a directory...
|
||||
if (!file.isFile()) {
|
||||
List<ResourceFile> propFiles =
|
||||
ExtensionUtils.findExtensionPropertyFiles(new ResourceFile(file), true);
|
||||
for (ResourceFile props : propFiles) {
|
||||
ExtensionDetails ext = ExtensionUtils.createExtensionDetailsFromPropertyFile(props);
|
||||
String version = ext.getVersion();
|
||||
if (version != null) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
private boolean installExtensions(List<File> files) {
|
||||
boolean didInstall = false;
|
||||
for (File file : files) {
|
||||
boolean success = ExtensionUtils.install(file);
|
||||
didInstall |= success;
|
||||
}
|
||||
|
||||
// If the given file is a zip...
|
||||
try {
|
||||
if (ExtensionUtils.isZip(file)) {
|
||||
Properties props = ExtensionUtils.getPropertiesFromArchive(file);
|
||||
if (props == null) {
|
||||
return null; // no prop file exists
|
||||
}
|
||||
ExtensionDetails ext = ExtensionUtils.createExtensionDetailsFromProperties(props);
|
||||
String version = ext.getVersion();
|
||||
if (version != null) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ExtensionException e) {
|
||||
// just fall through
|
||||
}
|
||||
return null;
|
||||
return didInstall;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,9 +200,8 @@ public class ExtensionTableProvider extends DialogComponentProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for a {@link GhidraFileChooser} that restricts selection to those
|
||||
* files that are Ghidra Extensions (zip files with an extension.properties
|
||||
* file) or folders.
|
||||
* Filter for a {@link GhidraFileChooser} that restricts selection to those files that are
|
||||
* Ghidra Extensions (zip files with an extension.properties file) or folders.
|
||||
*/
|
||||
private class ExtensionFileFilter implements GhidraFileFilter {
|
||||
@Override
|
||||
@ -269,16 +210,8 @@ public class ExtensionTableProvider extends DialogComponentProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(File f, GhidraFileChooserModel l_model) {
|
||||
|
||||
try {
|
||||
return ExtensionUtils.isExtension(new ResourceFile(f)) || f.isDirectory();
|
||||
}
|
||||
catch (ExtensionException e) {
|
||||
// if something fails to be recognized as an extension, just move on.
|
||||
}
|
||||
|
||||
return false;
|
||||
public boolean accept(File f, GhidraFileChooserModel model) {
|
||||
return f.isDirectory() || ExtensionUtils.isExtension(f);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,953 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.project.extensions;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import docking.widgets.OkDialog;
|
||||
import docking.widgets.OptionDialog;
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskLauncher;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import utilities.util.FileUtilities;
|
||||
import utility.application.ApplicationLayout;
|
||||
|
||||
/**
|
||||
* Utility class for managing Ghidra Extensions.
|
||||
* <p>
|
||||
* Extensions are defined as any archive or folder that contains an <code>extension.properties</code>
|
||||
* file. This properties file can contain the following attributes:
|
||||
* <ul>
|
||||
* <li>name (required)</li>
|
||||
* <li>description</li>
|
||||
* <li>author</li>
|
||||
* <li>createdOn (format: MM/dd/yyyy)</li>
|
||||
* <li>version</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Extensions may be installed/uninstalled by users at runtime, using the
|
||||
* {@link ExtensionTableProvider}. Installation consists of unzipping the extension archive to an
|
||||
* installation folder, currently <code>{ghidra user settings dir}/Extensions</code>. To uninstall,
|
||||
* the unpacked folder is simply removed.
|
||||
*/
|
||||
public class ExtensionUtils {
|
||||
|
||||
/** Magic number that identifies the first bytes of a ZIP archive. This is used to verify
|
||||
that a file is a zip rather than just checking the extension. */
|
||||
private static final int ZIPFILE = 0x504b0304;
|
||||
|
||||
public static String PROPERTIES_FILE_NAME = "extension.properties";
|
||||
public static String PROPERTIES_FILE_NAME_UNINSTALLED = "extension.properties.uninstalled";
|
||||
|
||||
private static final Logger log = LogManager.getLogger(ExtensionUtils.class);
|
||||
|
||||
/**
|
||||
* Performs extension maintenance. This should be called at startup, before any plugins or
|
||||
* extension points are loaded.
|
||||
*/
|
||||
public static void initializeExtensions() {
|
||||
|
||||
Extensions extensions = getAllInstalledExtensions();
|
||||
|
||||
// delete any extensions marked for removal
|
||||
extensions.cleanupExtensionsMarkedForRemoval();
|
||||
|
||||
// check for duplicates in the remaining extensions
|
||||
extensions.reportDuplicateExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all known extensions that have not been marked for removal.
|
||||
*
|
||||
* @return set of installed extensions
|
||||
*/
|
||||
public static Set<ExtensionDetails> getActiveInstalledExtensions() {
|
||||
Extensions extensions = getAllInstalledExtensions();
|
||||
return extensions.getActiveExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all installed extensions. These are all the extensions found in
|
||||
* {@link ApplicationLayout#getExtensionInstallationDirs}.
|
||||
*
|
||||
* @return set of installed extensions
|
||||
*/
|
||||
public static Set<ExtensionDetails> getInstalledExtensions() {
|
||||
Extensions extensions = getAllInstalledExtensions();
|
||||
return extensions.get();
|
||||
}
|
||||
|
||||
private static Extensions getAllInstalledExtensions() {
|
||||
|
||||
Extensions extensions = new Extensions();
|
||||
|
||||
// Find all extension.properties or extension.properties.uninstalled files in
|
||||
// the install directory and create a ExtensionDetails object for each.
|
||||
ApplicationLayout layout = Application.getApplicationLayout();
|
||||
for (ResourceFile installDir : layout.getExtensionInstallationDirs()) {
|
||||
if (!installDir.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.trace("Checking extension installation dir '" + installDir);
|
||||
|
||||
File dir = installDir.getFile(false);
|
||||
List<File> propFiles = findExtensionPropertyFiles(dir);
|
||||
for (File propFile : propFiles) {
|
||||
|
||||
ExtensionDetails extension = createExtensionFromProperties(propFile);
|
||||
if (extension == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We found this extension in the installation directory, so set the install path
|
||||
// property and add to the final set.
|
||||
File extInstallDir = propFile.getParentFile();
|
||||
extension.setInstallDir(extInstallDir);
|
||||
|
||||
log.trace("Loading extension '" + extension.getName() + "' from: " + extInstallDir);
|
||||
extensions.add(extension);
|
||||
}
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all archive extensions. These are all the extensions found in
|
||||
* {@link ApplicationLayout#getExtensionArchiveDir}. This are added to an installation as
|
||||
* part of the build processes.
|
||||
* <p>
|
||||
* Archived extensions may be zip files and directories.
|
||||
*
|
||||
* @return set of archive extensions
|
||||
*/
|
||||
public static Set<ExtensionDetails> getArchiveExtensions() {
|
||||
|
||||
log.trace("Finding archived extensions");
|
||||
|
||||
ApplicationLayout layout = Application.getApplicationLayout();
|
||||
ResourceFile archiveDir = layout.getExtensionArchiveDir();
|
||||
if (archiveDir == null) {
|
||||
log.trace("No extension archive dir found");
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
ResourceFile[] archiveFiles = archiveDir.listFiles();
|
||||
if (archiveFiles == null) {
|
||||
log.trace("No files in extension archive dir: " + archiveDir);
|
||||
return Collections.emptySet(); // no files or dirs inside of the archive directory
|
||||
}
|
||||
|
||||
Set<ExtensionDetails> extensions = new HashSet<>();
|
||||
findExtensionsInZips(archiveFiles, extensions);
|
||||
findExtensionsInFolder(archiveDir.getFile(false), extensions);
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
private static void findExtensionsInZips(ResourceFile[] archiveFiles,
|
||||
Set<ExtensionDetails> extensions) {
|
||||
for (ResourceFile file : archiveFiles) {
|
||||
ExtensionDetails extension = createExtensionDetailsFromArchive(file);
|
||||
if (extension == null) {
|
||||
log.trace("Skipping archive file; not an extension: " + file);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (extensions.contains(extension)) {
|
||||
log.error(
|
||||
"Skipping extension \"" + extension.getName() + "\" found at " +
|
||||
extension.getInstallPath() +
|
||||
".\nArchived extension by that name already found.");
|
||||
}
|
||||
extensions.add(extension);
|
||||
}
|
||||
}
|
||||
|
||||
private static void findExtensionsInFolder(File dir, Set<ExtensionDetails> extensions) {
|
||||
List<File> propFiles = findExtensionPropertyFiles(dir);
|
||||
for (File propFile : propFiles) {
|
||||
ExtensionDetails extension = createExtensionFromProperties(propFile);
|
||||
if (extension == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We found this extension in the installation directory, so set the archive path
|
||||
// property and add to the final set.
|
||||
File extDir = propFile.getParentFile();
|
||||
extension.setArchivePath(extDir.getAbsolutePath());
|
||||
|
||||
if (extensions.contains(extension)) {
|
||||
log.error(
|
||||
"Skipping duplicate extension \"" + extension.getName() + "\" found at " +
|
||||
extension.getInstallPath());
|
||||
}
|
||||
extensions.add(extension);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the given extension file. This can be either an archive (zip) or a directory that
|
||||
* contains an extension.properties file.
|
||||
*
|
||||
* @param file the extension to install
|
||||
* @return true if the extension was successfully installed
|
||||
*/
|
||||
public static boolean install(File file) {
|
||||
|
||||
log.trace("installing file " + file);
|
||||
|
||||
if (file == null) {
|
||||
log.error("Install file cannot be null");
|
||||
return false;
|
||||
}
|
||||
|
||||
ExtensionDetails extension = getExtension(file, false);
|
||||
if (extension == null) {
|
||||
Msg.showError(ExtensionUtils.class, null, "Error Installing Extension",
|
||||
file.getAbsolutePath() + " does not point to a valid ghidra extension");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkForConflictWithDevelopmentExtension(extension)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (checkForDuplicateExtensions(extension)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that the version of the extension is valid for this version of Ghidra. If not,
|
||||
// just exit without installing.
|
||||
if (!validateExtensionVersion(extension)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AtomicBoolean installed = new AtomicBoolean(false);
|
||||
TaskLauncher.launchModal("Installing Extension", (monitor) -> {
|
||||
installed.set(doRunInstallTask(extension, file, monitor));
|
||||
});
|
||||
|
||||
boolean success = installed.get();
|
||||
if (success) {
|
||||
log.trace("Finished installing " + file);
|
||||
}
|
||||
else {
|
||||
log.trace("Failed to install " + file);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private static boolean doRunInstallTask(ExtensionDetails extension, File file,
|
||||
TaskMonitor monitor) {
|
||||
try {
|
||||
if (file.isFile()) {
|
||||
return unzipToInstallationFolder(extension, file, monitor);
|
||||
}
|
||||
|
||||
return copyToInstallationFolder(file, monitor);
|
||||
}
|
||||
catch (CancelledException e) {
|
||||
log.info("Extension installation cancelled by user");
|
||||
}
|
||||
catch (IOException e) {
|
||||
Msg.showError(ExtensionUtils.class, null, "Error Installing Extension",
|
||||
"Unexpected error installing extension", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the given extension from its declared archive path
|
||||
* @param extension the extension
|
||||
* @return true if successful
|
||||
*/
|
||||
public static boolean installExtensionFromArchive(ExtensionDetails extension) {
|
||||
if (extension == null) {
|
||||
log.error("Extension to install cannot be null");
|
||||
return false;
|
||||
}
|
||||
|
||||
String archivePath = extension.getArchivePath();
|
||||
if (archivePath == null) {
|
||||
log.error(
|
||||
"Cannot install from archive; extension is missing archive path");
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplicationLayout layout = Application.getApplicationLayout();
|
||||
ResourceFile extInstallDir = layout.getExtensionInstallationDirs().get(0);
|
||||
String extName = extension.getName();
|
||||
File extDestinationDir = new ResourceFile(extInstallDir, extName).getFile(false);
|
||||
File archiveFile = new File(archivePath);
|
||||
if (install(archiveFile)) {
|
||||
extension.setInstallDir(new File(extDestinationDir, extName));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the given extension version to the current Ghidra version. If they are different,
|
||||
* then the user will be prompted to confirm the installation. This method will return true
|
||||
* if the versions match or the user has chosen to install anyway.
|
||||
*
|
||||
* @param extension the extension
|
||||
* @return true if the versions match or the user has chosen to install anyway
|
||||
*/
|
||||
private static boolean validateExtensionVersion(ExtensionDetails extension) {
|
||||
String extVersion = extension.getVersion();
|
||||
if (extVersion == null) {
|
||||
extVersion = "<no version>";
|
||||
}
|
||||
|
||||
String appVersion = Application.getApplicationVersion();
|
||||
if (extVersion.equals(appVersion)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String message = "Extension version mismatch.\nName: " + extension.getName() +
|
||||
"Extension version: " + extVersion + ".\nGhidra version: " + appVersion + ".";
|
||||
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
|
||||
"Extension Version Mismatch",
|
||||
message,
|
||||
"Install Anyway");
|
||||
if (choice != OptionDialog.OPTION_ONE) {
|
||||
log.info(removeNewlines(message + " Did not install"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String removeNewlines(String s) {
|
||||
return s.replaceAll("\n", " ");
|
||||
}
|
||||
|
||||
private static boolean checkForDuplicateExtensions(ExtensionDetails newExtension) {
|
||||
|
||||
Extensions extensions = getAllInstalledExtensions();
|
||||
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
|
||||
if (matches.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log.trace("Duplicate extensions found by name '" + newExtension.getName() + "'");
|
||||
|
||||
if (matches.size() > 1) {
|
||||
reportMultipleDuplicateExtensionsWhenInstalling(newExtension, matches);
|
||||
return true;
|
||||
}
|
||||
|
||||
ExtensionDetails installedExtension = matches.get(0);
|
||||
String message =
|
||||
"Attempting to install an extension matching the name of an existing extension.\n" +
|
||||
"New extension version: " + newExtension.getVersion() + ".\n" +
|
||||
"Installed extension version: " + installedExtension.getVersion() + ".\n\n" +
|
||||
"To install, click 'Remove Existing', restart Ghidra, then install again.";
|
||||
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
|
||||
"Duplicate Extension",
|
||||
message,
|
||||
"Remove Existing");
|
||||
|
||||
String installPath = installedExtension.getInstallPath();
|
||||
if (choice != OptionDialog.OPTION_ONE) {
|
||||
log.info(
|
||||
removeNewlines(
|
||||
message + " Skipping installation. Original extension still installed: " +
|
||||
installPath));
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// At this point the user would like to replace the existing extension. We cannot delete
|
||||
// the existing extension, as it may be in use; mark it for removal.
|
||||
//
|
||||
log.info(
|
||||
removeNewlines(
|
||||
message + " Installing new extension. Existing extension will be removed after " +
|
||||
"restart: " + installPath));
|
||||
installedExtension.markForUninstall();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void reportMultipleDuplicateExtensionsWhenInstalling(ExtensionDetails extension,
|
||||
List<ExtensionDetails> matches) {
|
||||
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
buffy.append("Found multiple duplicate extensions while trying to install '")
|
||||
.append(extension.getName())
|
||||
.append("'\n");
|
||||
for (ExtensionDetails otherExtension : matches) {
|
||||
buffy.append("Duplicate: " + otherExtension.getInstallPath()).append('\n');
|
||||
}
|
||||
buffy.append("Please close Ghidra and manually remove from these extensions from the " +
|
||||
"filesystem.");
|
||||
|
||||
Msg.showInfo(ExtensionUtils.class, null, "Duplicate Extensions Found", buffy.toString());
|
||||
}
|
||||
|
||||
private static void reportDuplicateExtensionsWhenLoading(String name,
|
||||
List<ExtensionDetails> extensions) {
|
||||
|
||||
ExtensionDetails loadedExtension = extensions.get(0);
|
||||
File loadedInstallDir = loadedExtension.getInstallDir();
|
||||
|
||||
for (int i = 1; i < extensions.size(); i++) {
|
||||
ExtensionDetails duplicate = extensions.get(i);
|
||||
log.info("Duplicate extension found '" + name + "'. Keeping extension from " +
|
||||
loadedInstallDir + ". Skipping extension found at " +
|
||||
duplicate.getInstallDir());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkForConflictWithDevelopmentExtension(ExtensionDetails newExtension) {
|
||||
|
||||
Extensions extensions = getAllInstalledExtensions();
|
||||
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
|
||||
if (matches.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ExtensionDetails extension : matches) {
|
||||
|
||||
if (extension.isInstalledInInstallationFolder()) {
|
||||
|
||||
OkDialog.showError("Duplicate Extensions Found",
|
||||
"Attempting to install an extension that conflicts with an extension located in " +
|
||||
"the Ghidra installation folder.\nYou must manually remove the existing " +
|
||||
"extension to install the new extension.\nExisting extension: " +
|
||||
extension.getInstallDir());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given file or directory is a valid ghidra extension.
|
||||
* <p>
|
||||
* Note: This means that the zip or directory contains an extension.properties file.
|
||||
*
|
||||
* @param file the zip or directory to inspect
|
||||
* @return true if the given file represents a valid extension
|
||||
*/
|
||||
public static boolean isExtension(File file) {
|
||||
return getExtension(file, true) != null;
|
||||
}
|
||||
|
||||
private static ExtensionDetails getExtension(File file, boolean quiet) {
|
||||
|
||||
if (file == null) {
|
||||
log.error("Cannot get an extension; null file");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return tryToGetExtension(file);
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (quiet) {
|
||||
log.trace("Exception trying to read an extension from " + file, e);
|
||||
}
|
||||
else {
|
||||
log.error("Exception trying to read an extension from " + file, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ExtensionDetails tryToGetExtension(File file) throws IOException {
|
||||
|
||||
if (file == null) {
|
||||
log.error("Cannot get an extension; null file");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (file.isDirectory() && file.canRead()) {
|
||||
File[] files = file.listFiles(f -> f.getName().equals(PROPERTIES_FILE_NAME));
|
||||
if (files != null && files.length == 1) {
|
||||
return tryToLoadExtensionFromProperties(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// If the given file is a zip, it's an extension if there's an extension.properties
|
||||
// file at the TOP LEVEL ONLY; we don't want to search for nested property files (this
|
||||
// would cause us to match things like the main ghidra distribution zip file.
|
||||
// eg: DatabaseTools/extension.properties is valid
|
||||
// DatabaseTools/foo/extension.properties is not.
|
||||
if (isZip(file)) {
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
Properties props = getProperties(zipFile);
|
||||
if (props != null) {
|
||||
return createExtensionDetails(props);
|
||||
}
|
||||
throw new IOException("No extension.properties file found in zip");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given file is a valid .zip archive.
|
||||
*
|
||||
* @param file the file to test
|
||||
* @return true if file is a valid zip
|
||||
*/
|
||||
private static boolean isZip(File file) {
|
||||
|
||||
if (file == null) {
|
||||
log.error("Cannot check for extension zip; null file");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.isDirectory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.length() < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (DataInputStream in =
|
||||
new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {
|
||||
int test = in.readInt();
|
||||
return test == ZIPFILE;
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.trace("Unable to check if file is a zip file: " + file + ". " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of files representing all the <code>extension.properties</code> files found
|
||||
* under a given directory. This will only search the immediate children of the given directory.
|
||||
* <p>
|
||||
* Searching the child directories of a directory allows clients to pick an extension parent
|
||||
* directory that contains multiple extension directories.
|
||||
*
|
||||
* @param installDir the directory that contains extension subdirectories
|
||||
* @return list of extension.properties files
|
||||
*/
|
||||
private static List<File> findExtensionPropertyFiles(File installDir) {
|
||||
|
||||
List<File> results = new ArrayList<>();
|
||||
FileUtilities.forEachFile(installDir, f -> {
|
||||
if (!f.isDirectory() || f.getName().equals("Skeleton")) {
|
||||
return;
|
||||
}
|
||||
|
||||
File pf = getPropertyFile(f);
|
||||
if (pf != null) {
|
||||
results.add(pf);
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an extension.properties or extension.properties.uninstalled file if the given
|
||||
* directory contains one.
|
||||
*
|
||||
* @param dir the directory to search
|
||||
* @return the file, or null if doesn't exist
|
||||
*/
|
||||
private static File getPropertyFile(File dir) {
|
||||
|
||||
File f = new File(dir, PROPERTIES_FILE_NAME_UNINSTALLED);
|
||||
if (f.exists()) {
|
||||
return f;
|
||||
}
|
||||
|
||||
f = new File(dir, PROPERTIES_FILE_NAME);
|
||||
if (f.exists()) {
|
||||
return f;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
|
||||
|
||||
File file = resourceFile.getFile(false);
|
||||
if (!isZip(file)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
Properties props = getProperties(zipFile);
|
||||
if (props != null) {
|
||||
ExtensionDetails extension = createExtensionDetails(props);
|
||||
extension.setArchivePath(file.getAbsolutePath());
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error(
|
||||
"Unable to read zip file to get extension properties: " + file, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Properties getProperties(ZipFile zipFile) throws IOException {
|
||||
|
||||
Properties props = null;
|
||||
Enumeration<ZipArchiveEntry> zipEntries = zipFile.getEntries();
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
ZipArchiveEntry entry = zipEntries.nextElement();
|
||||
Properties nextProperties = getProperties(zipFile, entry);
|
||||
if (nextProperties != null) {
|
||||
if (props != null) {
|
||||
throw new IOException(
|
||||
"Zip file contains multiple extension properties files");
|
||||
}
|
||||
props = nextProperties;
|
||||
}
|
||||
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
private static Properties getProperties(ZipFile zipFile, ZipArchiveEntry entry)
|
||||
throws IOException {
|
||||
// We only search for the property file at the top level
|
||||
String path = entry.getName();
|
||||
List<String> parts = FileUtilities.pathToParts(path);
|
||||
if (parts.size() != 2) { // require 2 parts: dir name / props file
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.getName().endsWith(PROPERTIES_FILE_NAME)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
InputStream propFile = zipFile.getInputStream(entry);
|
||||
Properties prop = new Properties();
|
||||
prop.load(propFile);
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the given folder to the extension install location. Any existing folder at that
|
||||
* location will be deleted.
|
||||
* <p>
|
||||
* Note: Any existing folder with the same name will be overwritten.
|
||||
*
|
||||
* @param sourceFolder the extension folder
|
||||
* @param monitor the task monitor
|
||||
* @return true if successful
|
||||
* @throws IOException if the delete or copy fails
|
||||
* @throws CancelledException if the user cancels the copy
|
||||
*/
|
||||
private static boolean copyToInstallationFolder(File sourceFolder, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
|
||||
log.trace("Copying extension from " + sourceFolder);
|
||||
|
||||
ApplicationLayout layout = Application.getApplicationLayout();
|
||||
ResourceFile installDir = layout.getExtensionInstallationDirs().get(0);
|
||||
File installDirRoot = installDir.getFile(false);
|
||||
File newDir = new File(installDirRoot, sourceFolder.getName());
|
||||
if (hasExistingExtension(newDir, monitor)) {
|
||||
return false;
|
||||
}
|
||||
FileUtilities.copyDir(sourceFolder, newDir, monitor);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean hasExistingExtension(File extensionFolder, TaskMonitor monitor) {
|
||||
|
||||
if (extensionFolder.exists()) {
|
||||
Msg.showWarn(ExtensionUtils.class, null, "Duplicate Extension Folder",
|
||||
"Attempting to install a new extension over an existing directory.\n" +
|
||||
"Either remove the extension for that directory from the UI\n" +
|
||||
"or close Ghidra and delete the directory and try installing again.\n\n" +
|
||||
"Directory: " + extensionFolder);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpacks a given zip file to {@link ApplicationLayout#getExtensionInstallationDirs}. The
|
||||
* file permissions in the original zip will be retained.
|
||||
* <p>
|
||||
* Note: This method uses the Apache zip files since they keep track of permissions info;
|
||||
* the built-in java objects (e.g., ZipEntry) do not.
|
||||
*
|
||||
* @param extension the extension
|
||||
* @param file the zip file to unpack
|
||||
* @param monitor the task monitor
|
||||
* @return true if successful
|
||||
* @throws IOException if any part of the unzipping fails, or if the target location is invalid
|
||||
* @throws CancelledException if the user cancels the unzip
|
||||
* @throws IOException if error unzipping zip file
|
||||
*/
|
||||
private static boolean unzipToInstallationFolder(ExtensionDetails extension, File file,
|
||||
TaskMonitor monitor)
|
||||
throws CancelledException, IOException {
|
||||
|
||||
log.trace("Unzipping extension from " + file);
|
||||
|
||||
ApplicationLayout layout = Application.getApplicationLayout();
|
||||
ResourceFile installDir = layout.getExtensionInstallationDirs().get(0);
|
||||
File installDirRoot = installDir.getFile(false);
|
||||
File destinationFolder = new File(installDirRoot, extension.getName());
|
||||
if (hasExistingExtension(destinationFolder, monitor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
|
||||
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
monitor.checkCancelled();
|
||||
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
String filePath = installDir + File.separator + entry.getName();
|
||||
File destination = new File(filePath);
|
||||
if (entry.isDirectory()) {
|
||||
destination.mkdirs();
|
||||
}
|
||||
else {
|
||||
writeZipEntryToFile(zipFile, entry, destination);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void writeZipEntryToFile(ZipFile zFile, ZipArchiveEntry entry, File destination)
|
||||
throws IOException {
|
||||
try (OutputStream outputStream =
|
||||
new BufferedOutputStream(new FileOutputStream(destination))) {
|
||||
|
||||
// Create the file at the new location...
|
||||
IOUtils.copy(zFile.getInputStream(entry), outputStream);
|
||||
|
||||
// ...and update its permissions. But only continue if the zip was created on a unix
|
||||
//platform. If not, we cannot use the posix libraries to set permissions.
|
||||
if (entry.getPlatform() != ZipArchiveEntry.PLATFORM_UNIX) {
|
||||
return;
|
||||
}
|
||||
|
||||
int mode = entry.getUnixMode();
|
||||
if (mode != 0) { // 0 indicates non-unix platform
|
||||
Set<PosixFilePermission> perms = getPermissions(mode);
|
||||
try {
|
||||
Files.setPosixFilePermissions(destination.toPath(), perms);
|
||||
}
|
||||
catch (UnsupportedOperationException e) {
|
||||
// Need to catch this, as Windows does not support the posix call. This is not
|
||||
// an error, however, and should just silently fail.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExtensionDetails tryToLoadExtensionFromProperties(File file) throws IOException {
|
||||
|
||||
Properties props = new Properties();
|
||||
try (InputStream in = new FileInputStream(file.getAbsolutePath())) {
|
||||
props.load(in);
|
||||
return createExtensionDetails(props);
|
||||
}
|
||||
}
|
||||
|
||||
private static ExtensionDetails createExtensionFromProperties(File file) {
|
||||
try {
|
||||
return tryToLoadExtensionFromProperties(file);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("Error loading extension properties from " + file.getAbsolutePath(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ExtensionDetails createExtensionDetails(Properties props) {
|
||||
|
||||
String name = props.getProperty("name");
|
||||
String desc = props.getProperty("description");
|
||||
String author = props.getProperty("author");
|
||||
String date = props.getProperty("createdOn");
|
||||
String version = props.getProperty("version");
|
||||
|
||||
return new ExtensionDetails(name, desc, author, date, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstalls a given extension.
|
||||
*
|
||||
* @param extension the extension to uninstall
|
||||
* @return true if successfully uninstalled
|
||||
*/
|
||||
private static boolean removeExtension(ExtensionDetails extension) {
|
||||
|
||||
if (extension == null) {
|
||||
log.error("Extension to uninstall cannot be null");
|
||||
return false;
|
||||
}
|
||||
|
||||
File installDir = extension.getInstallDir();
|
||||
if (installDir == null) {
|
||||
log.error("Extension installation path is not set; unable to delete files");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FileUtilities.deleteDir(installDir)) {
|
||||
extension.setInstallDir(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Unix permissions to a set of {@link PosixFilePermission}s.
|
||||
*
|
||||
* @param unixMode integer representation of file permissions
|
||||
* @return set of POSIX file permissions
|
||||
*/
|
||||
private static Set<PosixFilePermission> getPermissions(int unixMode) {
|
||||
|
||||
Set<PosixFilePermission> permissions = new HashSet<>();
|
||||
|
||||
if ((unixMode & 0400) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_READ);
|
||||
}
|
||||
if ((unixMode & 0200) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_WRITE);
|
||||
}
|
||||
if ((unixMode & 0100) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
}
|
||||
if ((unixMode & 0040) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_READ);
|
||||
}
|
||||
if ((unixMode & 0020) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_WRITE);
|
||||
}
|
||||
if ((unixMode & 0010) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_EXECUTE);
|
||||
}
|
||||
if ((unixMode & 0004) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_READ);
|
||||
}
|
||||
if ((unixMode & 0002) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_WRITE);
|
||||
}
|
||||
if ((unixMode & 0001) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of all extensions found. This class provides methods processing duplicates and
|
||||
* managing extensions marked for removal.
|
||||
*/
|
||||
private static class Extensions {
|
||||
|
||||
private Map<String, List<ExtensionDetails>> extensionsByName = new HashMap<>();
|
||||
|
||||
void add(ExtensionDetails e) {
|
||||
extensionsByName.computeIfAbsent(e.getName(), n -> new ArrayList<>()).add(e);
|
||||
}
|
||||
|
||||
Set<ExtensionDetails> getActiveExtensions() {
|
||||
return extensionsByName.values()
|
||||
.stream()
|
||||
.map(list -> list.get(0))
|
||||
.filter(ext -> !ext.isPendingUninstall())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
List<ExtensionDetails> getMatchingExtensions(ExtensionDetails extension) {
|
||||
return extensionsByName.computeIfAbsent(extension.getName(), name -> List.of());
|
||||
}
|
||||
|
||||
void cleanupExtensionsMarkedForRemoval() {
|
||||
|
||||
Set<String> names = new HashSet<>(extensionsByName.keySet());
|
||||
for (String name : names) {
|
||||
List<ExtensionDetails> extensions = extensionsByName.get(name);
|
||||
Iterator<ExtensionDetails> it = extensions.iterator();
|
||||
while (it.hasNext()) {
|
||||
ExtensionDetails extension = it.next();
|
||||
if (!extension.isPendingUninstall()) {
|
||||
continue;
|
||||
}
|
||||
if (!removeExtension(extension)) {
|
||||
log.error("Error removing extension: " + extension.getInstallPath());
|
||||
}
|
||||
|
||||
it.remove();
|
||||
}
|
||||
|
||||
if (extensions.isEmpty()) {
|
||||
extensionsByName.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void reportDuplicateExtensions() {
|
||||
|
||||
Set<Entry<String, List<ExtensionDetails>>> entries = extensionsByName.entrySet();
|
||||
for (Entry<String, List<ExtensionDetails>> entry : entries) {
|
||||
List<ExtensionDetails> list = entry.getValue();
|
||||
if (list.size() == 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
reportDuplicateExtensionsWhenLoading(entry.getKey(), list);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all unique (no duplicates) extensions that the application is aware of
|
||||
* @return the extensions
|
||||
*/
|
||||
Set<ExtensionDetails> get() {
|
||||
return extensionsByName.values()
|
||||
.stream()
|
||||
.map(list -> list.get(0))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.project.tool;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.widgets.OptionDialog;
|
||||
import generic.json.Json;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.dialog.PluginInstallerDialog;
|
||||
import ghidra.framework.plugintool.util.PluginDescription;
|
||||
import ghidra.framework.project.extensions.ExtensionDetails;
|
||||
import ghidra.framework.project.extensions.ExtensionUtils;
|
||||
import ghidra.util.NumericUtilities;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
* A class to manage saving and restoring of known extension used by this tool.
|
||||
*/
|
||||
class ExtensionManager {
|
||||
|
||||
private static final String EXTENSION_ATTRIBUTE_NAME_ENCODED = "ENCODED_NAME";
|
||||
private static final String EXTENSION_ATTRIBUTE_NAME = "NAME";
|
||||
private static final String EXTENSIONS_XML_NAME = "EXTENSIONS";
|
||||
private static final String EXTENSION_ELEMENT_NAME = "EXTENSION";
|
||||
|
||||
private PluginTool tool;
|
||||
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
|
||||
|
||||
ExtensionManager(PluginTool tool) {
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
void checkForNewExtensions() {
|
||||
if (newExtensionPlugins.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
propmtToConfigureNewPlugins(newExtensionPlugins);
|
||||
newExtensionPlugins.clear();
|
||||
}
|
||||
|
||||
private void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
|
||||
|
||||
// Offer the user a chance to configure any newly discovered plugins
|
||||
int option = OptionDialog.showYesNoDialog(tool.getToolFrame(), "New Plugins Found!",
|
||||
"New extension plugins detected. Would you like to configure them?");
|
||||
if (option == OptionDialog.YES_OPTION) {
|
||||
List<PluginDescription> pluginDescriptions = getPluginDescriptions(plugins);
|
||||
PluginInstallerDialog pluginInstaller = new PluginInstallerDialog("New Plugins Found!",
|
||||
tool, new PluginConfigurationModel(tool), pluginDescriptions);
|
||||
tool.showDialog(pluginInstaller);
|
||||
}
|
||||
}
|
||||
|
||||
void saveToXml(Element xml) {
|
||||
|
||||
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
|
||||
Element extensionsParent = new Element(EXTENSIONS_XML_NAME);
|
||||
for (ExtensionDetails ext : installedExtensions) {
|
||||
Element child = new Element(EXTENSION_ELEMENT_NAME);
|
||||
String name = ext.getName();
|
||||
if (XmlUtilities.hasInvalidXMLCharacters(name)) {
|
||||
child.setAttribute(EXTENSION_ATTRIBUTE_NAME_ENCODED, NumericUtilities
|
||||
.convertBytesToString(name.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
else {
|
||||
child.setAttribute(EXTENSION_ATTRIBUTE_NAME, name);
|
||||
}
|
||||
|
||||
extensionsParent.addContent(child);
|
||||
}
|
||||
|
||||
xml.addContent(extensionsParent);
|
||||
}
|
||||
|
||||
void restoreFromXml(Element xml) {
|
||||
|
||||
Set<ExtensionDetails> installedExtensions = getExtensions();
|
||||
if (installedExtensions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> knownExtensionNames = getKnownExtensions(xml);
|
||||
Set<ExtensionDetails> newExtensions = new HashSet<>(installedExtensions);
|
||||
for (ExtensionDetails ext : installedExtensions) {
|
||||
if (knownExtensionNames.contains(ext.getName())) {
|
||||
newExtensions.remove(ext);
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of all plugins contained in those extensions. If there are none, then either
|
||||
// none of the extensions has any plugins, or Ghidra hasn't been restarted since installing
|
||||
// the extension(s), so none of the plugin classes have been loaded. In either case, there
|
||||
// is nothing more to do.
|
||||
Set<Class<?>> newPlugins = findLoadedPlugins(newExtensions);
|
||||
newExtensionPlugins.addAll(newPlugins);
|
||||
}
|
||||
|
||||
private Set<ExtensionDetails> getExtensions() {
|
||||
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
|
||||
return installedExtensions.stream()
|
||||
.filter(e -> !e.isInstalledInInstallationFolder())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Set<String> getKnownExtensions(Element xml) {
|
||||
Set<String> knownExtensionNames = new HashSet<>();
|
||||
Element extensionsParent = xml.getChild(EXTENSIONS_XML_NAME);
|
||||
if (extensionsParent == null) {
|
||||
return knownExtensionNames;
|
||||
}
|
||||
|
||||
Iterator<?> it = extensionsParent.getChildren(EXTENSION_ELEMENT_NAME).iterator();
|
||||
while (it.hasNext()) {
|
||||
Element child = (Element) it.next();
|
||||
String encodedValue = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME_ENCODED);
|
||||
if (encodedValue != null) {
|
||||
byte[] bytes = NumericUtilities.convertStringToBytes(encodedValue);
|
||||
String decoded = new String(bytes, StandardCharsets.UTF_8);
|
||||
knownExtensionNames.add(decoded);
|
||||
}
|
||||
else {
|
||||
String name = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME);
|
||||
knownExtensionNames.add(name);
|
||||
}
|
||||
}
|
||||
return knownExtensionNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all {@link PluginDescription} objects that match a given set of plugin classes. This
|
||||
* effectively tells the caller which of the given plugins have been loaded by the class loader.
|
||||
* <p>
|
||||
* Note that this method does not take path/package information into account when finding
|
||||
* plugins; in the example above, if there is more than one plugin with the name "FooPlugin",
|
||||
* only one will be found (the one found is not guaranteed to be the first).
|
||||
*
|
||||
* @param plugins the list of plugin classes to search for
|
||||
* @return list of plugin descriptions
|
||||
*/
|
||||
private List<PluginDescription> getPluginDescriptions(Set<Class<?>> plugins) {
|
||||
|
||||
// First define the list of plugin descriptions to return
|
||||
List<PluginDescription> descriptions = new ArrayList<>();
|
||||
|
||||
// Get all plugins that have been loaded
|
||||
PluginsConfiguration pluginsConfiguration = tool.getPluginsConfiguration();
|
||||
List<PluginDescription> allPluginDescriptions =
|
||||
pluginsConfiguration.getManagedPluginDescriptions();
|
||||
|
||||
// see if an entry exists in the list of all loaded plugins
|
||||
for (Class<?> plugin : plugins) {
|
||||
String pluginName = plugin.getSimpleName();
|
||||
|
||||
Optional<PluginDescription> desc = allPluginDescriptions.stream()
|
||||
.filter(d -> (pluginName.equals(d.getName())))
|
||||
.findAny();
|
||||
if (desc.isPresent()) {
|
||||
descriptions.add(desc.get());
|
||||
}
|
||||
}
|
||||
|
||||
return descriptions;
|
||||
}
|
||||
|
||||
private static Set<Class<?>> findLoadedPlugins(Set<ExtensionDetails> extensions) {
|
||||
|
||||
Set<PluginPath> pluginPaths = getPluginPaths();
|
||||
Set<Class<?>> extensionPlugins = new HashSet<>();
|
||||
for (ExtensionDetails extension : extensions) {
|
||||
File installDir = extension.getInstallDir();
|
||||
if (installDir == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<Class<?>> classes = findPluginsLoadedFromExtension(installDir, pluginPaths);
|
||||
extensionPlugins.addAll(classes);
|
||||
}
|
||||
|
||||
return extensionPlugins;
|
||||
}
|
||||
|
||||
private static Set<PluginPath> getPluginPaths() {
|
||||
Set<PluginPath> paths = new HashSet<>();
|
||||
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
|
||||
for (Class<? extends Plugin> plugin : plugins) {
|
||||
paths.add(new PluginPath(plugin));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all plugin classes loaded from a particular extension folder.
|
||||
* <p>
|
||||
* This uses the {@link ClassSearcher} to find all <code>Plugin.class</code> objects on the
|
||||
* classpath. For each class, the original resource file is compared against the
|
||||
* given extension folder and the jar files for that extension.
|
||||
*
|
||||
* @param dir the directory to search, or a jar file
|
||||
* @param pluginPaths all loaded plugin paths
|
||||
* @return list of {@link Plugin} classes, or empty list if none found
|
||||
*/
|
||||
private static Set<Class<?>> findPluginsLoadedFromExtension(File dir,
|
||||
Set<PluginPath> pluginPaths) {
|
||||
|
||||
Set<Class<?>> result = new HashSet<>();
|
||||
|
||||
// Find any jar files in the directory provided
|
||||
Set<String> jarPaths = getJarPaths(dir);
|
||||
|
||||
// Now get all Plugin.class file paths and see if any of them were loaded from one of the
|
||||
// extension the given extension directory
|
||||
for (PluginPath pluginPath : pluginPaths) {
|
||||
if (pluginPath.isFrom(dir)) {
|
||||
result.add(pluginPath.getPluginClass());
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String jarPath : jarPaths) {
|
||||
if (pluginPath.isFrom(jarPath)) {
|
||||
result.add(pluginPath.getPluginClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Set<String> getJarPaths(File dir) {
|
||||
Set<File> jarFiles = new HashSet<>();
|
||||
findJarFiles(dir, jarFiles);
|
||||
Set<String> paths = new HashSet<>();
|
||||
for (File jar : jarFiles) {
|
||||
try {
|
||||
URL jarUrl = jar.toURI().toURL();
|
||||
paths.add(jarUrl.getPath());
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the given list with all discovered jar files found in the given directory and
|
||||
* its subdirectories.
|
||||
*
|
||||
* @param dir the directory to search
|
||||
* @param jarFiles list of found jar files
|
||||
*/
|
||||
private static void findJarFiles(File dir, Set<File> jarFiles) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
findJarFiles(f, jarFiles);
|
||||
}
|
||||
|
||||
if (f.isFile() && f.getName().endsWith(".jar")) {
|
||||
jarFiles.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PluginPath {
|
||||
private Class<? extends Plugin> pluginClass;
|
||||
private String pluginLocation;
|
||||
private File pluginFile;
|
||||
|
||||
PluginPath(Class<? extends Plugin> pluginClass) {
|
||||
this.pluginClass = pluginClass;
|
||||
String name = pluginClass.getName();
|
||||
URL url = pluginClass.getResource('/' + name.replace('.', '/') + ".class");
|
||||
this.pluginLocation = url.getPath();
|
||||
this.pluginFile = new File(pluginLocation);
|
||||
}
|
||||
|
||||
public boolean isFrom(File dir) {
|
||||
return FileUtilities.isPathContainedWithin(dir, pluginFile);
|
||||
}
|
||||
|
||||
boolean isFrom(String jarPath) {
|
||||
return pluginLocation.contains(jarPath);
|
||||
}
|
||||
|
||||
Class<? extends Plugin> getPluginClass() {
|
||||
return pluginClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Json.toString(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ package ghidra.framework.project.tool;
|
||||
|
||||
import ghidra.framework.main.ApplicationLevelOnlyPlugin;
|
||||
import ghidra.framework.plugintool.Plugin;
|
||||
import ghidra.framework.plugintool.util.PluginsConfiguration;
|
||||
import ghidra.framework.plugintool.PluginsConfiguration;
|
||||
|
||||
/**
|
||||
* A configuration that allows all general plugins and application plugins. Plugins that may only
|
||||
|
@ -15,10 +15,6 @@
|
||||
*/
|
||||
package ghidra.framework.project.tool;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.jdom.Element;
|
||||
|
||||
import docking.ActionContext;
|
||||
@ -30,13 +26,9 @@ import docking.widgets.OptionDialog;
|
||||
import ghidra.app.util.FileOpenDropHandler;
|
||||
import ghidra.framework.model.Project;
|
||||
import ghidra.framework.model.ToolTemplate;
|
||||
import ghidra.framework.options.PreferenceState;
|
||||
import ghidra.framework.plugintool.PluginConfigurationModel;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.dialog.*;
|
||||
import ghidra.framework.plugintool.util.*;
|
||||
import ghidra.framework.plugintool.PluginsConfiguration;
|
||||
import ghidra.util.HelpLocation;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
* Tool created by the workspace when the user chooses to create a new
|
||||
@ -47,16 +39,14 @@ public class GhidraTool extends PluginTool {
|
||||
|
||||
private static final String NON_AUTOSAVE_SAVE_TOOL_TITLE = "Save Tool?";
|
||||
|
||||
// Preference category stored in the tools' xml file, indicating which extensions
|
||||
// this tool is aware of. This is used to recognize when new extensions have been
|
||||
// installed that the user should be made aware of.
|
||||
public static final String EXTENSIONS_PREFERENCE_NAME = "KNOWN_EXTENSIONS";
|
||||
|
||||
public static boolean autoSave = true;
|
||||
|
||||
private FileOpenDropHandler fileOpenDropHandler;
|
||||
private DockingAction configureToolAction;
|
||||
|
||||
private ExtensionManager extensionManager;
|
||||
private boolean hasBeenShown;
|
||||
|
||||
/**
|
||||
* Construct a new Ghidra Tool.
|
||||
*
|
||||
@ -77,6 +67,18 @@ public class GhidraTool extends PluginTool {
|
||||
super(project, template);
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to do this here, since our parent constructor calls methods on us that need the
|
||||
* extension manager.
|
||||
* @return the extension manager
|
||||
*/
|
||||
private ExtensionManager getExtensionManager() {
|
||||
if (extensionManager == null) {
|
||||
extensionManager = new ExtensionManager(this);
|
||||
}
|
||||
return extensionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DockingWindowManager createDockingWindowManager(boolean isDockable, boolean hasStatus,
|
||||
boolean isModal) {
|
||||
@ -122,6 +124,31 @@ public class GhidraTool extends PluginTool {
|
||||
winMgr.restoreWindowDataFromXml(rootElement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Element saveToXml(boolean includeConfigState) {
|
||||
Element xml = super.saveToXml(includeConfigState);
|
||||
getExtensionManager().saveToXml(xml);
|
||||
return xml;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean restoreFromXml(Element root) {
|
||||
boolean success = super.restoreFromXml(root);
|
||||
getExtensionManager().restoreFromXml(root);
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean visible) {
|
||||
if (visible) {
|
||||
if (!hasBeenShown) { // first time being shown
|
||||
getExtensionManager().checkForNewExtensions();
|
||||
}
|
||||
hasBeenShown = true;
|
||||
}
|
||||
super.setVisible(visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSave() {
|
||||
if (autoSave) {
|
||||
@ -207,175 +234,6 @@ public class GhidraTool extends PluginTool {
|
||||
}
|
||||
|
||||
protected void showConfig() {
|
||||
// if (hasUnsavedData()) {
|
||||
// OptionDialog.showWarningDialog( getToolFrame(),"Configure Not Allowed!",
|
||||
// "The tool has unsaved data. Configuring the tool can potentially lose\n"+
|
||||
// "data. Therefore, this operation is not allowed with unsaved data.\n\n"+
|
||||
// "Please save your data before configuring the tool.");
|
||||
// return;
|
||||
// }
|
||||
showConfig(true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for extensions that have been installed since the last time this tool
|
||||
* was launched. If any are found, and if those extensions contain plugins, the user is
|
||||
* notified and given the chance to install them.
|
||||
*
|
||||
*/
|
||||
public void checkForNewExtensions() {
|
||||
|
||||
// 1. First remove any extensions that are in the tool preferences that are no longer
|
||||
// installed. This will happen if the user installs an extension, launches
|
||||
// a tool, then uninstalls the extension.
|
||||
removeUninstalledExtensions();
|
||||
|
||||
// 2. Now figure out which extensions have been added.
|
||||
Set<ExtensionDetails> newExtensions =
|
||||
ExtensionUtils.getExtensionsInstalledSinceLastToolLaunch(this);
|
||||
|
||||
// 3. Get a list of all plugins contained in those extensions. If there are none, then
|
||||
// either none of the extensions has any plugins, or Ghidra hasn't been restarted since
|
||||
// installing the extension(s), so none of the plugin classes have been loaded. In
|
||||
// either case, there is nothing more to do.
|
||||
List<Class<?>> newPlugins = PluginUtils.findLoadedPlugins(newExtensions);
|
||||
if (newPlugins.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Notify the user there are new plugins.
|
||||
int option = OptionDialog.showYesNoDialog(getActiveWindow(), "New Plugins Found!",
|
||||
"New extension plugins detected. Would you like to configure them?");
|
||||
if (option == OptionDialog.YES_OPTION) {
|
||||
List<PluginDescription> pluginDescriptions = getPluginDescriptions(this, newPlugins);
|
||||
PluginInstallerDialog pluginInstaller = new PluginInstallerDialog("New Plugins Found!",
|
||||
this, new PluginConfigurationModel(this), pluginDescriptions);
|
||||
showDialog(pluginInstaller);
|
||||
}
|
||||
|
||||
// 5. Update the preference file to reflect the new extensions now known to this tool.
|
||||
addInstalledExtensions(newExtensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all {@link PluginDescription} objects that match a given set of plugin classes. This
|
||||
* effectively tells the caller which of the given plugins have been loaded by the class loader.
|
||||
* <p>
|
||||
* Note that this method does not take path/package information into account when finding
|
||||
* plugins; in the example above, if there is more than one plugin with the name "FooPlugin",
|
||||
* only one will be found (the one found is not guaranteed to be the first).
|
||||
*
|
||||
* @param tool the current tool
|
||||
* @param plugins the list of plugin classes to search for
|
||||
* @return list of plugin descriptions
|
||||
*/
|
||||
private List<PluginDescription> getPluginDescriptions(PluginTool tool, List<Class<?>> plugins) {
|
||||
|
||||
// First define the list of plugin descriptions to return
|
||||
List<PluginDescription> retPlugins = new ArrayList<>();
|
||||
|
||||
// Get all plugins that have been loaded
|
||||
PluginsConfiguration pluginClassManager = getPluginsConfiguration();
|
||||
List<PluginDescription> allPluginDescriptions =
|
||||
pluginClassManager.getManagedPluginDescriptions();
|
||||
|
||||
// see if an entry exists in the list of all loaded plugins
|
||||
for (Class<?> plugin : plugins) {
|
||||
String pluginName = plugin.getSimpleName();
|
||||
|
||||
Optional<PluginDescription> desc = allPluginDescriptions.stream()
|
||||
.filter(d -> (pluginName.equals(d.getName())))
|
||||
.findAny();
|
||||
if (desc.isPresent()) {
|
||||
retPlugins.add(desc.get());
|
||||
}
|
||||
}
|
||||
|
||||
return retPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes any extensions in the tool preferences that are no longer installed.
|
||||
*/
|
||||
private void removeUninstalledExtensions() {
|
||||
|
||||
try {
|
||||
// Get all installed extensions
|
||||
Set<ExtensionDetails> installedExtensions =
|
||||
ExtensionUtils.getInstalledExtensions(false);
|
||||
List<String> installedExtensionNames =
|
||||
installedExtensions.stream().map(ext -> ext.getName()).collect(Collectors.toList());
|
||||
|
||||
// Get the list of extensions in the tool preference state
|
||||
DockingWindowManager dockingWindowManager =
|
||||
DockingWindowManager.getInstance(getToolFrame());
|
||||
|
||||
PreferenceState state = getExtensionPreferences(dockingWindowManager);
|
||||
|
||||
String[] extNames = state.getStrings(EXTENSIONS_PREFERENCE_NAME, new String[0]);
|
||||
List<String> preferenceExtensionNames = new ArrayList<>(Arrays.asList(extNames));
|
||||
|
||||
// Now see if any extensions are in the current preferences that are NOT in the installed extensions
|
||||
// list. Those are the ones we need to remove.
|
||||
for (Iterator<String> i = preferenceExtensionNames.iterator(); i.hasNext();) {
|
||||
String extName = i.next();
|
||||
if (!installedExtensionNames.contains(extName)) {
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, put the new extension list in the preferences object
|
||||
state.putStrings(EXTENSIONS_PREFERENCE_NAME,
|
||||
preferenceExtensionNames.toArray(new String[preferenceExtensionNames.size()]));
|
||||
dockingWindowManager.putPreferenceState(EXTENSIONS_PREFERENCE_NAME, state);
|
||||
}
|
||||
catch (ExtensionException e) {
|
||||
// This is a problem but isn't catastrophic. Just warn the user and continue.
|
||||
Msg.warn(this, "Couldn't retrieve installed extensions!", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the preferences for this tool with a set of new extensions.
|
||||
*
|
||||
* @param newExtensions the extensions to add
|
||||
*/
|
||||
private void addInstalledExtensions(Set<ExtensionDetails> newExtensions) {
|
||||
|
||||
DockingWindowManager dockingWindowManager =
|
||||
DockingWindowManager.getInstance(getToolFrame());
|
||||
|
||||
// Get the current preference object. We need to get the existing prefs so we can add our
|
||||
// new extensions to them. If the extensions category doesn't exist yet, just create one.
|
||||
PreferenceState state = getExtensionPreferences(dockingWindowManager);
|
||||
|
||||
// Now get the list of extensions already in the prefs...
|
||||
String[] extNames = state.getStrings(EXTENSIONS_PREFERENCE_NAME, new String[0]);
|
||||
|
||||
// ...and parse the passed-in extension list to get just the names of the extensions to add.
|
||||
List<String> extensionNamesToAdd =
|
||||
newExtensions.stream().map(ext -> ext.getName()).collect(Collectors.toList());
|
||||
|
||||
// Finally add them together and update the preference state.
|
||||
String[] allPreferences = ArrayUtils.addAll(extNames,
|
||||
extensionNamesToAdd.toArray(new String[extensionNamesToAdd.size()]));
|
||||
state.putStrings(EXTENSIONS_PREFERENCE_NAME, allPreferences);
|
||||
dockingWindowManager.putPreferenceState(EXTENSIONS_PREFERENCE_NAME, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the extensions portion of the preferences object.
|
||||
*
|
||||
* @param dockingWindowManager the docking window manager
|
||||
* @return the extensions portion of the preference state, or a new preference state object if no extension section exists
|
||||
*/
|
||||
private PreferenceState getExtensionPreferences(DockingWindowManager dockingWindowManager) {
|
||||
|
||||
PreferenceState state = dockingWindowManager.getPreferenceState(EXTENSIONS_PREFERENCE_NAME);
|
||||
if (state == null) {
|
||||
state = new PreferenceState();
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ import ghidra.framework.data.*;
|
||||
import ghidra.framework.main.AppInfo;
|
||||
import ghidra.framework.main.FrontEndTool;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.plugintool.PluginEvent;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.preferences.Preferences;
|
||||
import ghidra.framework.protocol.ghidra.GetUrlContentTypeTask;
|
||||
@ -169,27 +168,6 @@ class ToolServicesImpl implements ToolServices {
|
||||
return toolChest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void displaySimilarTool(PluginTool tool, DomainFile domainFile, PluginEvent event) {
|
||||
|
||||
PluginTool[] similarTools = getSameNamedRunningTools(tool);
|
||||
PluginTool matchingTool = findToolUsingFile(similarTools, domainFile);
|
||||
if (matchingTool != null) {
|
||||
// Bring the matching tool forward.
|
||||
matchingTool.toFront();
|
||||
}
|
||||
else {
|
||||
// Create a new tool and pop it up.
|
||||
Workspace workspace = toolManager.getActiveWorkspace();
|
||||
matchingTool = workspace.runTool(tool.getToolTemplate(true));
|
||||
matchingTool.setVisible(true);
|
||||
matchingTool.acceptDomainFiles(new DomainFile[] { domainFile });
|
||||
}
|
||||
|
||||
// Fire the indicated event in the tool.
|
||||
matchingTool.firePluginEvent(event);
|
||||
}
|
||||
|
||||
private static DefaultLaunchMode getDefaultLaunchMode() {
|
||||
DefaultLaunchMode defaultLaunchMode = DefaultLaunchMode.DEFAULT;
|
||||
FrontEndTool frontEndTool = AppInfo.getFrontEndTool();
|
||||
|
@ -79,14 +79,9 @@ class WorkspaceImpl implements Workspace {
|
||||
PluginTool tool = toolManager.getTool(this, template);
|
||||
if (tool != null) {
|
||||
tool.setVisible(true);
|
||||
|
||||
if (tool instanceof GhidraTool) {
|
||||
GhidraTool gTool = (GhidraTool) tool;
|
||||
gTool.checkForNewExtensions();
|
||||
}
|
||||
runningTools.add(tool);
|
||||
|
||||
// alert the tool manager that we changed
|
||||
// alert the tool manager that we have changed
|
||||
toolManager.setWorkspaceChanged(this);
|
||||
toolManager.fireToolAddedEvent(this, tool);
|
||||
}
|
||||
@ -161,6 +156,7 @@ class WorkspaceImpl implements Workspace {
|
||||
String defaultTool = System.getProperty("ghidra.defaulttool");
|
||||
if (defaultTool != null && !defaultTool.equals("")) {
|
||||
PluginTool tool = toolManager.getTool(defaultTool);
|
||||
tool.setVisible(isActive);
|
||||
runningTools.add(tool);
|
||||
toolManager.fireToolAddedEvent(this, tool);
|
||||
return;
|
||||
@ -175,27 +171,23 @@ class WorkspaceImpl implements Workspace {
|
||||
}
|
||||
|
||||
PluginTool tool = toolManager.getTool(toolName);
|
||||
if (tool != null) {
|
||||
tool.setVisible(isActive);
|
||||
|
||||
if (tool instanceof GhidraTool) {
|
||||
GhidraTool gTool = (GhidraTool) tool;
|
||||
gTool.checkForNewExtensions();
|
||||
}
|
||||
|
||||
boolean hadChanges = tool.hasConfigChanged();
|
||||
tool.restoreWindowingDataFromXml(element);
|
||||
|
||||
Element toolDataElem = element.getChild("DATA_STATE");
|
||||
tool.restoreDataStateFromXml(toolDataElem);
|
||||
if (hadChanges) {
|
||||
// restore the dirty state, which is cleared by the restoreDataState call
|
||||
tool.setConfigChanged(true);
|
||||
}
|
||||
|
||||
runningTools.add(tool);
|
||||
toolManager.fireToolAddedEvent(this, tool);
|
||||
if (tool == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tool.setVisible(isActive);
|
||||
boolean hadChanges = tool.hasConfigChanged();
|
||||
tool.restoreWindowingDataFromXml(element);
|
||||
|
||||
Element toolDataElem = element.getChild("DATA_STATE");
|
||||
tool.restoreDataStateFromXml(toolDataElem);
|
||||
if (hadChanges) {
|
||||
// restore the dirty state, which is cleared by the restoreDataState call
|
||||
tool.setConfigChanged(true);
|
||||
}
|
||||
|
||||
runningTools.add(tool);
|
||||
toolManager.fireToolAddedEvent(this, tool);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,6 @@
|
||||
*/
|
||||
package ghidra.framework.plugintool;
|
||||
|
||||
import ghidra.framework.plugintool.util.PluginsConfiguration;
|
||||
|
||||
/**
|
||||
* A dummy version of {@link PluginTool} that tests can use when they need an instance of
|
||||
* the PluginTool, but do not wish to use a real version
|
||||
|
@ -0,0 +1,747 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.framework.project.extensions;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.junit.*;
|
||||
|
||||
import docking.DialogComponentProvider;
|
||||
import docking.test.AbstractDockingTest;
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.framework.Application;
|
||||
import utilities.util.FileUtilities;
|
||||
import utility.application.ApplicationLayout;
|
||||
import utility.function.ExceptionalCallback;
|
||||
import utility.module.ModuleUtilities;
|
||||
|
||||
/**
|
||||
* Tests for the {@link ExtensionUtils} class.
|
||||
*/
|
||||
public class ExtensionUtilsTest extends AbstractDockingTest {
|
||||
|
||||
private static final String BUILD_FOLDER_NAME = "TestExtensionParentDir";
|
||||
private static final String TEST_EXT_NAME = "test";
|
||||
|
||||
private ApplicationLayout appLayout;
|
||||
|
||||
/*
|
||||
* Create dummy archive and installation folders in the temp space that we can populate
|
||||
* with extensions.
|
||||
*/
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
|
||||
// to see tracing; must set the 'console' appender to trace to see these
|
||||
// Configurator.setLevel("ghidra.framework.project.extensions", Level.TRACE);
|
||||
|
||||
setErrorGUIEnabled(false);
|
||||
|
||||
appLayout = Application.getApplicationLayout();
|
||||
|
||||
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
|
||||
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
|
||||
FileUtilities.deleteDir(installDir.getFile(false));
|
||||
}
|
||||
|
||||
createExtensionDirs();
|
||||
}
|
||||
|
||||
private static <E extends Exception> void errorsExpected(ExceptionalCallback<E> c)
|
||||
throws Exception {
|
||||
try {
|
||||
setErrorsExpected(true);
|
||||
c.call();
|
||||
}
|
||||
finally {
|
||||
setErrorsExpected(false);
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
sleep(1000);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that we can install an extension from a .zip file.
|
||||
*/
|
||||
@Test
|
||||
public void testInstallExtensionFromZip() throws IOException {
|
||||
|
||||
// Create an extension and install it.
|
||||
File file = createExtensionZip(TEST_EXT_NAME);
|
||||
ExtensionUtils.install(file);
|
||||
|
||||
// Verify there is something in the installation directory and it has the correct name
|
||||
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that we can install an extension from a folder.
|
||||
*/
|
||||
@Test
|
||||
public void testInstallArchiveExtensionFromFolder() throws IOException {
|
||||
|
||||
// Create an extension and install it.
|
||||
File file = createExtensionFolderInArchiveDir();
|
||||
ExtensionUtils.install(file);
|
||||
|
||||
// Verify the extension is in the install folder and has the correct name
|
||||
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsExtension_Zip_ValidZip() throws IOException {
|
||||
File zipFile1 = createExtensionZip(TEST_EXT_NAME);
|
||||
assertTrue(ExtensionUtils.isExtension(zipFile1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsExtension_Zip_InvalidZip() throws Exception {
|
||||
|
||||
errorsExpected(() -> {
|
||||
File zipFile2 = createNonExtensionZip(TEST_EXT_NAME);
|
||||
assertFalse(ExtensionUtils.isExtension(zipFile2));
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that we can recognize when a directory represents an extension.
|
||||
* <p>
|
||||
* Note: The presence of an extensions.properties file is the difference.
|
||||
*/
|
||||
@Test
|
||||
public void testIsExtension_Folder() throws Exception {
|
||||
File extDir = createTempDirectory("TestExtFolder");
|
||||
new File(extDir, "extension.properties").createNewFile();
|
||||
assertTrue(ExtensionUtils.isExtension(extDir));
|
||||
|
||||
errorsExpected(() -> {
|
||||
File nonExtDir = createTempDirectory("TestNonExtFolder");
|
||||
assertFalse(ExtensionUtils.isExtension(nonExtDir));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBadInputs() throws Exception {
|
||||
errorsExpected(() -> {
|
||||
assertFalse(ExtensionUtils.isExtension(null));
|
||||
assertFalse(ExtensionUtils.install(new File("this/file/does/not/exist")));
|
||||
assertFalse(ExtensionUtils.install(null));
|
||||
assertFalse(ExtensionUtils.installExtensionFromArchive(null));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallExtensionFromArchive() throws Exception {
|
||||
File zipFile = createExtensionZip(TEST_EXT_NAME);
|
||||
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
|
||||
extension.setArchivePath(zipFile.getAbsolutePath());
|
||||
String ghidraVersion = Application.getApplicationVersion();
|
||||
extension.setVersion(ghidraVersion);
|
||||
assertTrue(ExtensionUtils.installExtensionFromArchive(extension));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallExtensionFromZipArchive_VersionMismatch_Cancel() throws Exception {
|
||||
|
||||
File zipFile = createExtensionZip(TEST_EXT_NAME, "v2");
|
||||
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
|
||||
extension.setArchivePath(zipFile.getAbsolutePath());
|
||||
|
||||
AtomicBoolean didInstall = new AtomicBoolean();
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
|
||||
});
|
||||
|
||||
DialogComponentProvider confirmDialog =
|
||||
waitForDialogComponent("Extension Version Mismatch");
|
||||
pressButtonByText(confirmDialog, "Cancel");
|
||||
|
||||
assertFalse(didInstall.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallExtensionFromZipArchive_VersionMismatch_Continue() throws Exception {
|
||||
|
||||
File zipFile = createExtensionZip(TEST_EXT_NAME, "v2");
|
||||
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
|
||||
extension.setArchivePath(zipFile.getAbsolutePath());
|
||||
|
||||
AtomicBoolean didInstall = new AtomicBoolean();
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
|
||||
});
|
||||
|
||||
DialogComponentProvider confirmDialog =
|
||||
waitForDialogComponent("Extension Version Mismatch");
|
||||
pressButtonByText(confirmDialog, "Install Anyway");
|
||||
|
||||
assertFalse(didInstall.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallExtensionFromZipArchive_NullVersion() throws Exception {
|
||||
|
||||
File zipFile = createExtensionZip(TEST_EXT_NAME, null);
|
||||
ExtensionDetails extension = new TestExtensionDetails(TEST_EXT_NAME);
|
||||
extension.setVersion(null);
|
||||
extension.setArchivePath(zipFile.getAbsolutePath());
|
||||
|
||||
AtomicBoolean didInstall = new AtomicBoolean();
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
|
||||
});
|
||||
|
||||
DialogComponentProvider confirmDialog =
|
||||
waitForDialogComponent("Extension Version Mismatch");
|
||||
pressButtonByText(confirmDialog, "Cancel");
|
||||
|
||||
assertFalse(didInstall.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMarkForUninstall_ClearMark() throws Exception {
|
||||
|
||||
File externalFolder = createExternalExtensionInFolder();
|
||||
assertTrue(ExtensionUtils.install(externalFolder));
|
||||
|
||||
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
|
||||
|
||||
extension.markForUninstall();
|
||||
checkMarkForUninstall(extension);
|
||||
assertFalse(extension.isInstalled());
|
||||
|
||||
// Also test that we can clear the uninstall marker
|
||||
extension.clearMarkForUninstall();
|
||||
assertExtensionInstalled(TEST_EXT_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanupUninstalledExtions_WithExtensionMarkedForUninstall() throws Exception {
|
||||
|
||||
File externalFolder = createExternalExtensionInFolder();
|
||||
assertTrue(ExtensionUtils.install(externalFolder));
|
||||
|
||||
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
|
||||
|
||||
extension.markForUninstall();
|
||||
checkMarkForUninstall(extension);
|
||||
assertFalse(extension.isInstalled());
|
||||
|
||||
// Also test that we can clear the uninstall marker
|
||||
ExtensionUtils.initializeExtensions();
|
||||
checkCleanInstall();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanupUninstalledExtions_SomeExtensionMarkedForUninstall() throws Exception {
|
||||
|
||||
List<File> extensionFolders = createTwoExternalExtensionsInFolder();
|
||||
assertTrue(ExtensionUtils.install(extensionFolders.get(0)));
|
||||
assertTrue(ExtensionUtils.install(extensionFolders.get(1)));
|
||||
|
||||
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
|
||||
assertEquals(extensions.size(), 2);
|
||||
|
||||
Iterator<ExtensionDetails> it = extensions.iterator();
|
||||
ExtensionDetails extensionToRemove = it.next();
|
||||
ExtensionDetails extensionToKeep = it.next();
|
||||
assertTrue(extensionToRemove.isInstalled());
|
||||
|
||||
extensionToRemove.markForUninstall();
|
||||
checkMarkForUninstall(extensionToRemove);
|
||||
assertFalse(extensionToRemove.isInstalled());
|
||||
|
||||
// Also test that we can clear the uninstall marker
|
||||
ExtensionUtils.initializeExtensions();
|
||||
assertExtensionInstalled(extensionToKeep.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanupUninstalledExtions_NoExtensionsMarkedForUninstall() throws Exception {
|
||||
|
||||
File externalFolder = createExternalExtensionInFolder();
|
||||
assertTrue(ExtensionUtils.install(externalFolder));
|
||||
assertExtensionInstalled(TEST_EXT_NAME);
|
||||
|
||||
// This should not uninstall any extensions
|
||||
ExtensionUtils.initializeExtensions();
|
||||
assertExtensionInstalled(TEST_EXT_NAME);
|
||||
}
|
||||
|
||||
//=================================================================================================
|
||||
// Edge Cases
|
||||
//=================================================================================================
|
||||
|
||||
@Test
|
||||
public void testInstallingNewExtension_SameName_NewVersion() throws Exception {
|
||||
|
||||
// install extension Foo with Ghidra version
|
||||
File buildFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
String appVersion = Application.getApplicationVersion();
|
||||
File extensionFolder =
|
||||
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
|
||||
assertTrue(ExtensionUtils.install(extensionFolder));
|
||||
|
||||
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
|
||||
assertEquals(extensions.size(), 1);
|
||||
ExtensionDetails installedExtension = extensions.iterator().next();
|
||||
|
||||
// create another extension Foo v2
|
||||
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
|
||||
String newVersion = "v2";
|
||||
File extensionFolder2 =
|
||||
doCreateExternalExtensionInFolder(buildFolder2, TEST_EXT_NAME, newVersion);
|
||||
|
||||
AtomicBoolean didInstall = new AtomicBoolean();
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.install(extensionFolder2));
|
||||
});
|
||||
|
||||
DialogComponentProvider confirmDialog =
|
||||
waitForDialogComponent("Duplicate Extension");
|
||||
pressButtonByText(confirmDialog, "Remove Existing");
|
||||
|
||||
waitForSwing();
|
||||
assertFalse(didInstall.get());
|
||||
checkMarkForUninstall(installedExtension);
|
||||
|
||||
// run again after choosing to replace the installed extension
|
||||
ExtensionUtils.initializeExtensions(); // removed marked extensions
|
||||
checkCleanInstall();
|
||||
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.install(extensionFolder2));
|
||||
});
|
||||
|
||||
// no longer an installed extension conflict; now we have a version mismatch
|
||||
confirmDialog = waitForDialogComponent("Extension Version Mismatch");
|
||||
pressButtonByText(confirmDialog, "Install Anyway");
|
||||
|
||||
waitFor(didInstall);
|
||||
assertExtensionInstalled(TEST_EXT_NAME, newVersion);
|
||||
assertExtensionNotInstalled(TEST_EXT_NAME, appVersion);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallingNewExtension_SameName_NewVersion_Cancel() throws Exception {
|
||||
|
||||
// install extension Foo with Ghidra version
|
||||
File buildFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
String appVersion = Application.getApplicationVersion();
|
||||
File extensionFolder =
|
||||
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
|
||||
assertTrue(ExtensionUtils.install(extensionFolder));
|
||||
|
||||
// create another extension Foo v2
|
||||
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
|
||||
String newVersion = "v2";
|
||||
File extensionFolder2 =
|
||||
doCreateExternalExtensionInFolder(buildFolder2, TEST_EXT_NAME, newVersion);
|
||||
|
||||
AtomicBoolean didInstall = new AtomicBoolean();
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.install(extensionFolder2));
|
||||
});
|
||||
|
||||
DialogComponentProvider confirmDialog =
|
||||
waitForDialogComponent("Duplicate Extension");
|
||||
pressButtonByText(confirmDialog, "Cancel");
|
||||
waitForSwing();
|
||||
|
||||
assertExtensionInstalled(TEST_EXT_NAME, appVersion);
|
||||
assertExtensionNotInstalled(TEST_EXT_NAME, newVersion);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallingNewExtension_SameName_SaveVersion() throws Exception {
|
||||
|
||||
// install extension Foo with Ghidra version
|
||||
File buildFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
String appVersion = Application.getApplicationVersion();
|
||||
File extensionFolder =
|
||||
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
|
||||
assertTrue(ExtensionUtils.install(extensionFolder));
|
||||
|
||||
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
|
||||
assertEquals(extensions.size(), 1);
|
||||
ExtensionDetails installedExtension = extensions.iterator().next();
|
||||
|
||||
// create another extension Foo v2
|
||||
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
|
||||
String newVersion = appVersion;
|
||||
File extensionFolder2 =
|
||||
doCreateExternalExtensionInFolder(buildFolder2, TEST_EXT_NAME, newVersion);
|
||||
|
||||
AtomicBoolean didInstall = new AtomicBoolean();
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.install(extensionFolder2));
|
||||
});
|
||||
|
||||
DialogComponentProvider confirmDialog =
|
||||
waitForDialogComponent("Duplicate Extension");
|
||||
pressButtonByText(confirmDialog, "Remove Existing");
|
||||
|
||||
waitForSwing();
|
||||
assertFalse(didInstall.get());
|
||||
checkMarkForUninstall(installedExtension);
|
||||
|
||||
// run again after choosing to replace the installed extension
|
||||
ExtensionUtils.initializeExtensions(); // removed marked extensions
|
||||
checkCleanInstall();
|
||||
|
||||
runSwingLater(() -> {
|
||||
didInstall.set(ExtensionUtils.install(extensionFolder2));
|
||||
});
|
||||
|
||||
waitFor(didInstall);
|
||||
assertExtensionInstalled(TEST_EXT_NAME, newVersion);
|
||||
assertEquals(1, ExtensionUtils.getInstalledExtensions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallingNewExtension_FromZip_ZipHasMultipleExtensions() throws Exception {
|
||||
|
||||
// test that we can detect when a zip has more than one extension inside (as determined
|
||||
// by multiple properties files 1 level down from the root with different folder names
|
||||
|
||||
/*
|
||||
Create a zip file that looks something like this:
|
||||
|
||||
/
|
||||
{Extension Name 1}/
|
||||
extension.properties
|
||||
|
||||
{Extension Name 2}/
|
||||
extension.properties
|
||||
|
||||
*/
|
||||
|
||||
errorsExpected(() -> {
|
||||
File zipFile = createZipWithMultipleExtensions();
|
||||
assertFalse(ExtensionUtils.install(zipFile));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInstallThenUninstallThenReinstallWhenExtensionNameDoesntMatchFolder()
|
||||
throws Exception {
|
||||
|
||||
// This tests a previous failure case where an extension could not be reinstalled if its
|
||||
// name did not match the folder it was installed into. This could happen because the code
|
||||
// that installed the extension did not match the code to clear the 'mark for uninstall'
|
||||
// condition.
|
||||
|
||||
String nameProperty = "ExtensionNamedFoo";
|
||||
File externalFolder = createExtensionWithMismatchingNamePropertyString(nameProperty);
|
||||
assertTrue(ExtensionUtils.install(externalFolder));
|
||||
|
||||
ExtensionDetails extension = assertExtensionInstalled(nameProperty);
|
||||
|
||||
extension.markForUninstall();
|
||||
checkMarkForUninstall(extension);
|
||||
assertFalse(extension.isInstalled());
|
||||
|
||||
// Also test that we can clear the uninstall marker
|
||||
extension.clearMarkForUninstall();
|
||||
assertExtensionInstalled(nameProperty);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Private Methods
|
||||
//==================================================================================================
|
||||
|
||||
private ExtensionDetails assertExtensionInstalled(String name) {
|
||||
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
|
||||
Optional<ExtensionDetails> match =
|
||||
extensions.stream().filter(e -> e.getName().equals(name)).findFirst();
|
||||
assertTrue("No extension installed named '" + name + "'", match.isPresent());
|
||||
ExtensionDetails extension = match.get();
|
||||
assertTrue(extension.isInstalled());
|
||||
return extension;
|
||||
}
|
||||
|
||||
private ExtensionDetails assertExtensionInstalled(String name, String version) {
|
||||
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
|
||||
Optional<ExtensionDetails> match =
|
||||
extensions.stream().filter(e -> e.getName().equals(name)).findFirst();
|
||||
assertTrue("No extension installed named '" + name + "'", match.isPresent());
|
||||
ExtensionDetails extension = match.get();
|
||||
assertEquals(version, extension.getVersion());
|
||||
assertTrue(extension.isInstalled());
|
||||
return extension;
|
||||
}
|
||||
|
||||
private void assertExtensionNotInstalled(String name, String version) {
|
||||
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
|
||||
Optional<ExtensionDetails> match =
|
||||
extensions.stream()
|
||||
.filter(e -> e.getName().equals(name) && e.getVersion().equals(version))
|
||||
.findFirst();
|
||||
assertFalse("Extension should not be installed: '" + name + "'", match.isPresent());
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates the extension archive and installation directories.
|
||||
*
|
||||
* @throws IOException if there's an error creating the directories
|
||||
*/
|
||||
private void createExtensionDirs() throws IOException {
|
||||
|
||||
ResourceFile extensionDir = appLayout.getExtensionArchiveDir();
|
||||
if (!extensionDir.exists()) {
|
||||
if (!extensionDir.mkdir()) {
|
||||
throw new IOException("Failed to create extension archive directory for test");
|
||||
}
|
||||
}
|
||||
|
||||
ResourceFile installDir = appLayout.getExtensionInstallationDirs().get(0);
|
||||
if (!installDir.exists()) {
|
||||
if (!installDir.mkdir()) {
|
||||
throw new IOException("Failed to create extension installation directory for test");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that the installation folder is empty.
|
||||
*/
|
||||
private boolean checkCleanInstall() {
|
||||
ResourceFile[] files = appLayout.getExtensionInstallationDirs().get(0).listFiles();
|
||||
return (files == null || files.length == 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that the installation folder is not empty and contains a folder with the given name.
|
||||
*
|
||||
* @param name the name of the installed extension
|
||||
*/
|
||||
private void checkExtensionInstalledInFilesystem(String name) {
|
||||
ResourceFile[] files = appLayout.getExtensionInstallationDirs().get(0).listFiles();
|
||||
assertTrue(files.length >= 1);
|
||||
assertEquals(files[0].getName(), name);
|
||||
}
|
||||
|
||||
private void checkMarkForUninstall(ExtensionDetails extension) {
|
||||
checkMarkForUninstall(extension.getInstallDir());
|
||||
}
|
||||
|
||||
private void checkMarkForUninstall(File extensionDir) {
|
||||
File propertiesFile = new File(extensionDir, ExtensionUtils.PROPERTIES_FILE_NAME);
|
||||
assertFalse(propertiesFile.exists());
|
||||
File markedPropertiesFile =
|
||||
new File(extensionDir, ExtensionUtils.PROPERTIES_FILE_NAME_UNINSTALLED);
|
||||
assertTrue(markedPropertiesFile.exists());
|
||||
}
|
||||
|
||||
/*
|
||||
* Creates a valid extension in the archive folder. This extension is not a .zip, but a folder.
|
||||
*
|
||||
* @return the file representing the extension
|
||||
* @throws IOException if there's an error creating the extension
|
||||
*/
|
||||
private File createExtensionFolderInArchiveDir() throws IOException {
|
||||
|
||||
ResourceFile root = new ResourceFile(appLayout.getExtensionArchiveDir(), TEST_EXT_NAME);
|
||||
root.mkdir();
|
||||
|
||||
// Have to add a prop file so this will be recognized as an extension
|
||||
File propFile = new ResourceFile(root, "extension.properties").getFile(false);
|
||||
assertTrue(propFile.createNewFile());
|
||||
|
||||
Properties props = new Properties();
|
||||
props.put("name", TEST_EXT_NAME);
|
||||
props.put("description", "This is a description for " + TEST_EXT_NAME);
|
||||
props.put("author", "First Last");
|
||||
props.put("createdOn", new SimpleDateFormat("MM/dd/yyyy").format(new Date()));
|
||||
props.put("version", Application.getApplicationVersion());
|
||||
|
||||
try (OutputStream os = new FileOutputStream(propFile)) {
|
||||
props.store(os, null);
|
||||
}
|
||||
|
||||
return root.getFile(false);
|
||||
}
|
||||
|
||||
private File createExternalExtensionInFolder() throws Exception {
|
||||
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
return doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME);
|
||||
}
|
||||
|
||||
private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName)
|
||||
throws Exception {
|
||||
String version = Application.getApplicationVersion();
|
||||
return doCreateExternalExtensionInFolder(externalFolder, extensionName, version);
|
||||
}
|
||||
|
||||
private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName,
|
||||
String version)
|
||||
throws Exception {
|
||||
return doCreateExternalExtensionInFolder(externalFolder, extensionName, extensionName,
|
||||
version);
|
||||
}
|
||||
|
||||
private File createExtensionWithMismatchingNamePropertyString(String nameProperty)
|
||||
throws Exception {
|
||||
|
||||
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
String version = Application.getApplicationVersion();
|
||||
return doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME, nameProperty,
|
||||
version);
|
||||
}
|
||||
|
||||
private File doCreateExternalExtensionInFolder(File externalFolder,
|
||||
String extensionName, String nameProperty, String version)
|
||||
throws Exception {
|
||||
ResourceFile root = new ResourceFile(new ResourceFile(externalFolder), extensionName);
|
||||
root.mkdir();
|
||||
|
||||
// Have to add a prop file so this will be recognized as an extension
|
||||
File propFile = new ResourceFile(root, "extension.properties").getFile(false);
|
||||
assertTrue(propFile.createNewFile());
|
||||
Properties props = new Properties();
|
||||
props.put("name", nameProperty);
|
||||
props.put("description", "This is a description for " + extensionName);
|
||||
props.put("author", "First Last");
|
||||
props.put("createdOn", new SimpleDateFormat("MM/dd/yyyy").format(new Date()));
|
||||
props.put("version", version);
|
||||
|
||||
try (OutputStream os = new FileOutputStream(propFile)) {
|
||||
props.store(os, null);
|
||||
}
|
||||
|
||||
File manifest = new ResourceFile(root, ModuleUtilities.MANIFEST_FILE_NAME).getFile(false);
|
||||
manifest.createNewFile();
|
||||
|
||||
return root.getFile(false);
|
||||
}
|
||||
|
||||
private List<File> createTwoExternalExtensionsInFolder() throws Exception {
|
||||
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
File extension1 = doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME);
|
||||
File extension2 = doCreateExternalExtensionInFolder(externalFolder, TEST_EXT_NAME + "Two");
|
||||
return List.of(extension1, extension2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a generic zip that is a valid extension archive.
|
||||
*
|
||||
* @param zipName name of the zip to create
|
||||
* @return a zip file
|
||||
* @throws IOException if there's an error creating the zip
|
||||
*/
|
||||
private File createExtensionZip(String zipName) throws IOException {
|
||||
|
||||
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
String version = Application.getApplicationVersion();
|
||||
File f = new File(externalFolder, zipName + ".zip");
|
||||
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
|
||||
out.putNextEntry(new ZipEntry(zipName + "/"));
|
||||
out.putNextEntry(new ZipEntry(zipName + "/extension.properties"));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("name:").append(zipName).append('\n');
|
||||
sb.append("version:").append(version).append('\n');
|
||||
byte[] data = sb.toString().getBytes();
|
||||
out.write(data, 0, data.length);
|
||||
out.closeEntry();
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
private File createExtensionZip(String zipName, String version) throws IOException {
|
||||
|
||||
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
File f = new File(externalFolder, zipName + ".zip");
|
||||
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
|
||||
out.putNextEntry(new ZipEntry(zipName + "/"));
|
||||
out.putNextEntry(new ZipEntry(zipName + "/extension.properties"));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("name:").append(zipName).append('\n');
|
||||
sb.append("version:").append(version).append('\n');
|
||||
byte[] data = sb.toString().getBytes();
|
||||
out.write(data, 0, data.length);
|
||||
out.closeEntry();
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
private File createZipWithMultipleExtensions() throws IOException {
|
||||
|
||||
String zipName1 = "Foo";
|
||||
String zipName2 = "Bar";
|
||||
File externalFolder = createTempDirectory(BUILD_FOLDER_NAME);
|
||||
File f = new File(externalFolder, "MultiExtension.zip");
|
||||
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
|
||||
out.putNextEntry(new ZipEntry(zipName1 + "/"));
|
||||
out.putNextEntry(new ZipEntry(zipName1 + "/extension.properties"));
|
||||
|
||||
out.putNextEntry(new ZipEntry(zipName2 + "/"));
|
||||
out.putNextEntry(new ZipEntry(zipName2 + "/extension.properties"));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("name:MultiExtension");
|
||||
byte[] data = sb.toString().getBytes();
|
||||
out.write(data, 0, data.length);
|
||||
out.closeEntry();
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a generic zip that is NOT a valid extension archive (because it doesn't
|
||||
* have an extension.properties file).
|
||||
*
|
||||
* @param zipName name of the zip to create
|
||||
* @return a zip file
|
||||
* @throws IOException if there's an error creating the zip
|
||||
*/
|
||||
private File createNonExtensionZip(String zipName) throws IOException {
|
||||
|
||||
File f = new File(appLayout.getExtensionArchiveDir().getFile(false), zipName + ".zip");
|
||||
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(f))) {
|
||||
out.putNextEntry(new ZipEntry(zipName + "/"));
|
||||
out.putNextEntry(new ZipEntry(zipName + "/randomFile.txt"));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("name:" + zipName);
|
||||
byte[] data = sb.toString().getBytes();
|
||||
out.write(data, 0, data.length);
|
||||
out.closeEntry();
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
private class TestExtensionDetails extends ExtensionDetails {
|
||||
TestExtensionDetails(String name) {
|
||||
super(name, "Description", "Author", "01/01/01", "1.0");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -31,9 +31,9 @@ import docking.actions.PopupActionProvider;
|
||||
import docking.util.image.ToolIconURL;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.options.ToolOptions;
|
||||
import ghidra.framework.plugintool.PluginEvent;
|
||||
import ghidra.framework.plugintool.PluginTool;
|
||||
import ghidra.framework.plugintool.util.*;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.ServiceListener;
|
||||
import ghidra.framework.plugintool.util.UndoRedoToolState;
|
||||
import ghidra.program.model.listing.Program;
|
||||
|
||||
public class DummyTool extends PluginTool {
|
||||
@ -425,7 +425,7 @@ public class DummyTool extends PluginTool {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginsConfiguration createPluginsConfigurations() {
|
||||
public PluginsConfiguration createPluginsConfigurations() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,8 @@ import java.net.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
* Class for representing file object regardless of whether they are actual files in the file system or
|
||||
* or files stored inside of a jar file. This class provides most all the same capabilities as the
|
||||
@ -30,7 +32,7 @@ import java.util.Map;
|
||||
public class ResourceFile implements Comparable<ResourceFile> {
|
||||
private static final String JAR_FILE_PREFIX = "jar:file:";
|
||||
private Resource resource;
|
||||
private static Map<String, JarResource> jarRootsMap = new HashMap<String, JarResource>();
|
||||
private static Map<String, JarResource> jarRootsMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Construct a ResourceFile that represents a normal file in the file system.
|
||||
@ -121,6 +123,7 @@ public class ResourceFile implements Comparable<ResourceFile> {
|
||||
/**
|
||||
* Returns the canonical file path for this file.
|
||||
* @return the absolute file path for this file.
|
||||
* @throws IOException if an exception is thrown getting the canonical path
|
||||
*/
|
||||
public String getCanonicalPath() throws IOException {
|
||||
return resource.getCanonicalPath();
|
||||
@ -128,7 +131,7 @@ public class ResourceFile implements Comparable<ResourceFile> {
|
||||
|
||||
/**
|
||||
* Returns a array of ResourceFiles if this ResourceFile is a directory. Otherwise return null.
|
||||
* @return the child ResourceFiles if this is a directory, null otherwise.
|
||||
* @return the child ResourceFiles if this is a directory, null otherwise.
|
||||
*/
|
||||
public ResourceFile[] listFiles() {
|
||||
return resource.listFiles();
|
||||
@ -190,7 +193,7 @@ public class ResourceFile implements Comparable<ResourceFile> {
|
||||
* contents.
|
||||
* @return an InputStream for the file's contents.
|
||||
* @throws FileNotFoundException if the file does not exist.
|
||||
* @throws IOException
|
||||
* @throws IOException if an exception occurs creating the input stream
|
||||
*/
|
||||
public InputStream getInputStream() throws FileNotFoundException, IOException {
|
||||
return resource.getInputStream();
|
||||
@ -329,4 +332,13 @@ public class ResourceFile implements Comparable<ResourceFile> {
|
||||
public URI toURI() {
|
||||
return resource.toURI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this file's path contains the entire path of the given file.
|
||||
* @param otherFile the other file to check
|
||||
* @return true if this file's path contains the entire path of the given file.
|
||||
*/
|
||||
public boolean containsPath(ResourceFile otherFile) {
|
||||
return FileUtilities.isPathContainedWithin(getFile(false), otherFile.getFile(false));
|
||||
}
|
||||
}
|
||||
|
@ -155,27 +155,20 @@ public class GhidraApplicationLayout extends ApplicationLayout {
|
||||
|
||||
// Find installed extension modules
|
||||
for (ResourceFile extensionInstallDir : extensionInstallationDirs) {
|
||||
File[] extensionModuleDirs =
|
||||
extensionInstallDir.getFile(false).listFiles(d -> d.isDirectory());
|
||||
if (extensionModuleDirs != null) {
|
||||
for (File extensionModuleDir : extensionModuleDirs) {
|
||||
|
||||
// Skip extensions that live in an application root directory...we've already
|
||||
// found those.
|
||||
if (applicationRootDirs.stream()
|
||||
.anyMatch(dir -> FileUtilities.isPathContainedWithin(dir.getFile(false),
|
||||
extensionModuleDir))) {
|
||||
continue;
|
||||
}
|
||||
// Skip extensions slated for cleanup
|
||||
if (new File(extensionModuleDir, ModuleUtilities.MANIFEST_FILE_NAME_UNINSTALLED)
|
||||
.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
moduleRootDirectories.add(new ResourceFile(extensionModuleDir));
|
||||
FileUtilities.forEachFile(extensionInstallDir, extensionDir -> {
|
||||
// Skip extensions in an application root directory... already found those.
|
||||
if (FileUtilities.isPathContainedWithin(applicationRootDirs, extensionDir)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip extensions slated for cleanup
|
||||
if (ModuleUtilities.isUninstalled(extensionDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
moduleRootDirectories.add(extensionDir);
|
||||
});
|
||||
}
|
||||
|
||||
// Examine the classpath to look for modules outside of the application root directories.
|
||||
@ -189,11 +182,8 @@ public class GhidraApplicationLayout extends ApplicationLayout {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip classpath entries that live in an application root directory...we've already
|
||||
// found those.
|
||||
if (applicationRootDirs.stream()
|
||||
.anyMatch(dir -> FileUtilities.isPathContainedWithin(dir.getFile(false),
|
||||
classpathEntry.getFile(false)))) {
|
||||
// Skip extensions in an application root directory... already found those.
|
||||
if (FileUtilities.isPathContainedWithin(applicationRootDirs, classpathEntry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -231,7 +221,7 @@ public class GhidraApplicationLayout extends ApplicationLayout {
|
||||
* Returns the directory where all Ghidra extension archives are stored.
|
||||
* This should be at the following location:<br>
|
||||
* <ul>
|
||||
* <li><code>[application root]/Extensions/Ghidra</code></li>
|
||||
* <li><code>{install dir}/Extensions/Ghidra</code></li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the archive folder, or null if can't be determined
|
||||
@ -253,8 +243,8 @@ public class GhidraApplicationLayout extends ApplicationLayout {
|
||||
* should be at the following locations:<br>
|
||||
* <ul>
|
||||
* <li><code>[user settings dir]/Extensions</code></li>
|
||||
* <li><code>[application install dir]/Ghidra/Extensions</code></li>
|
||||
* <li><code>ghidra/Ghidra/Extensions</code> (development mode)</li>
|
||||
* <li><code>[application install dir]/Ghidra/Extensions</code> (Release Mode)</li>
|
||||
* <li><code>ghidra/Ghidra/Extensions</code> (Development Mode)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the install folder, or null if can't be determined
|
||||
@ -262,21 +252,16 @@ public class GhidraApplicationLayout extends ApplicationLayout {
|
||||
protected List<ResourceFile> findExtensionInstallationDirectories() {
|
||||
|
||||
List<ResourceFile> dirs = new ArrayList<>();
|
||||
dirs.add(new ResourceFile(new File(userSettingsDir, "Extensions")));
|
||||
|
||||
// Would like to find a better way to do this, but for the moment this seems the
|
||||
// only solution. We want to get the 'Extensions' directory in ghidra, but there's
|
||||
// no way to retrieve that directory directly. We can only get the full set of
|
||||
// application root dirs and search for it, hoping we don't encounter one with the
|
||||
// name 'Extensions' in one of the other root dirs.
|
||||
if (SystemUtilities.isInDevelopmentMode()) {
|
||||
ResourceFile rootDir = getApplicationRootDirs().iterator().next();
|
||||
File temp = new File(rootDir.getFile(false), "Extensions");
|
||||
if (temp.exists()) {
|
||||
dirs.add(new ResourceFile(temp));
|
||||
dirs.add(new ResourceFile(temp)); // ghidra/Ghidra/Extensions
|
||||
}
|
||||
}
|
||||
else {
|
||||
dirs.add(new ResourceFile(new File(userSettingsDir, "Extensions")));
|
||||
dirs.add(new ResourceFile(applicationInstallationDir, "Ghidra/Extensions"));
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import java.util.stream.Collectors;
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.framework.GModule;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import utilities.util.FileUtilities;
|
||||
import utility.application.ApplicationLayout;
|
||||
import utility.module.ModuleUtilities;
|
||||
|
||||
@ -115,7 +116,7 @@ public class GhidraLauncher {
|
||||
|
||||
// Get application layout
|
||||
GhidraApplicationLayout layout = new GhidraApplicationLayout();
|
||||
|
||||
|
||||
// Get the classpath
|
||||
List<String> classpathList = buildClasspath(layout);
|
||||
|
||||
@ -148,6 +149,7 @@ public class GhidraLauncher {
|
||||
addModuleJarPaths(classpathList, modules);
|
||||
}
|
||||
|
||||
addExtensionJarPaths(classpathList, modules, layout);
|
||||
addExternalJarPaths(classpathList, layout.getApplicationRootDirs());
|
||||
}
|
||||
else {
|
||||
@ -185,7 +187,7 @@ public class GhidraLauncher {
|
||||
* @param modules The modules to get the bin directories of.
|
||||
*/
|
||||
private static void addModuleBinPaths(List<String> pathList, Map<String, GModule> modules) {
|
||||
Collection<ResourceFile> dirs = ModuleUtilities.getModuleBinDirectories(modules);
|
||||
Collection<ResourceFile> dirs = ModuleUtilities.getModuleBinDirectories(modules.values());
|
||||
dirs.forEach(d -> pathList.add(d.getAbsolutePath()));
|
||||
}
|
||||
|
||||
@ -196,10 +198,45 @@ public class GhidraLauncher {
|
||||
* @param modules The modules to get the jars of.
|
||||
*/
|
||||
private static void addModuleJarPaths(List<String> pathList, Map<String, GModule> modules) {
|
||||
Collection<ResourceFile> dirs = ModuleUtilities.getModuleLibDirectories(modules);
|
||||
Collection<ResourceFile> dirs = ModuleUtilities.getModuleLibDirectories(modules.values());
|
||||
dirs.forEach(d -> pathList.addAll(findJarsInDir(d)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add extension module lib jars to the given path list. (This only needed in dev mode to find
|
||||
* any pre-built extensions that have been installed, since we already find extension module
|
||||
* jars in production mode.)
|
||||
*
|
||||
* @param pathList The list of paths to add to.
|
||||
* @param modules The modules to get the jars of.
|
||||
* @param layout the application layout.
|
||||
*/
|
||||
private static void addExtensionJarPaths(List<String> pathList,
|
||||
Map<String, GModule> modules, GhidraApplicationLayout layout) {
|
||||
|
||||
List<ResourceFile> extensionInstallationDirs = layout.getExtensionInstallationDirs();
|
||||
for (GModule module : modules.values()) {
|
||||
|
||||
ResourceFile moduleDir = module.getModuleRoot();
|
||||
if (!FileUtilities.isPathContainedWithin(extensionInstallationDirs, moduleDir)) {
|
||||
continue; // not an extension
|
||||
}
|
||||
|
||||
Collection<ResourceFile> libDirs =
|
||||
ModuleUtilities.getModuleLibDirectories(Set.of(module));
|
||||
if (libDirs.size() != 1) {
|
||||
continue; // assume multiple lib dirs signals a non-built development project
|
||||
}
|
||||
|
||||
// We have one lib dir; the name 'lib' is used for a fully built extension. Grab all
|
||||
// jars from the built extensions lib directory.
|
||||
ResourceFile dir = libDirs.iterator().next();
|
||||
if (dir.getName().equals("lib")) {
|
||||
pathList.addAll(findJarsInDir(dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add external runtime lib jars to the given path list. The external jars are discovered by
|
||||
* parsing the build/libraryDependencies.txt file that results from a prepDev.
|
||||
|
@ -850,7 +850,7 @@ public final class FileUtilities {
|
||||
*
|
||||
* @param potentialParentFile The file that may be the parent
|
||||
* @param otherFile The file that may be the child
|
||||
* @return boolean true if otherFile's path is within potentialParentFile's path.
|
||||
* @return boolean true if otherFile's path is within potentialParentFile's path
|
||||
*/
|
||||
public static boolean isPathContainedWithin(File potentialParentFile, File otherFile) {
|
||||
try {
|
||||
@ -871,6 +871,20 @@ public final class FileUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any of the given <code>potentialParents</code> is the parent path of or has
|
||||
* the same path as the given <code>otherFile</code>.
|
||||
*
|
||||
* @param potentialParents The files that may be the parent
|
||||
* @param otherFile The file that may be the child
|
||||
* @return boolean true if otherFile's path is within any of the potentialParents' paths
|
||||
*/
|
||||
public static boolean isPathContainedWithin(Collection<ResourceFile> potentialParents,
|
||||
ResourceFile otherFile) {
|
||||
|
||||
return potentialParents.stream().anyMatch(parent -> parent.containsPath(otherFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the portion of the second file that trails the full path of the first file. If
|
||||
* the paths are the same or unrelated, then null is returned.
|
||||
@ -1250,14 +1264,56 @@ public final class FileUtilities {
|
||||
* @param consumer the consumer of each child in the given directory
|
||||
* @throws IOException if there is any problem reading the directory contents
|
||||
*/
|
||||
public static void forEachFile(Path path, Consumer<Stream<Path>> consumer) throws IOException {
|
||||
|
||||
public static void forEachFile(Path path, Consumer<Path> consumer) throws IOException {
|
||||
if (!Files.isDirectory(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> pathStream = Files.list(path)) {
|
||||
consumer.accept(pathStream);
|
||||
pathStream.forEach(consumer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method to list the contents of the given directory path and pass each to the
|
||||
* given consumer. If the given path does not represent a directory, nothing will happen.
|
||||
*
|
||||
* @param resourceFile the directory
|
||||
* @param consumer the consumer of each child in the given directory
|
||||
*/
|
||||
public static void forEachFile(File resourceFile, Consumer<File> consumer) {
|
||||
if (!resourceFile.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File[] files = resourceFile.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (File child : files) {
|
||||
consumer.accept(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience method to list the contents of the given directory path and pass each to the
|
||||
* given consumer. If the given path does not represent a directory, nothing will happen.
|
||||
*
|
||||
* @param resourceFile the directory
|
||||
* @param consumer the consumer of each child in the given directory
|
||||
*/
|
||||
public static void forEachFile(ResourceFile resourceFile, Consumer<ResourceFile> consumer) {
|
||||
if (!resourceFile.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceFile[] files = resourceFile.listFiles();
|
||||
if (files == null) {
|
||||
return;
|
||||
}
|
||||
for (ResourceFile child : files) {
|
||||
consumer.accept(child);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -109,7 +109,15 @@ public abstract class ApplicationLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where archived application Extensions are stored.
|
||||
* Returns the directory where archived application Extensions are stored. This directory may
|
||||
* contain both zip files and subdirectories. This directory is only used inside of an
|
||||
* installation; development mode does not use this directory. This directory is used to ship
|
||||
* pre-built Ghidra extensions as part of a distribution.
|
||||
* <P>
|
||||
* This should be at the following location:<br>
|
||||
* <ul>
|
||||
* <li><code>{install dir}/Extensions/Ghidra</code></li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the application Extensions archive directory. Could be null if the
|
||||
* {@link ApplicationLayout} does not support application Extensions.
|
||||
@ -120,7 +128,13 @@ public abstract class ApplicationLayout {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link List ordered list} of the application Extensions installation directories.
|
||||
* Returns a prioritized {@link List ordered list} of the application Extensions installation
|
||||
* directories. Typically, the values may be any of the following locations:<br>
|
||||
* <ul>
|
||||
* <li><code>[user settings dir]/Extensions</code></li>
|
||||
* <li><code>[application install dir]/Ghidra/Extensions</code> (Release Mode)</li>
|
||||
* <li><code>ghidra/Ghidra/Extensions</code> (Development Mode)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return an {@link List ordered list} of the application Extensions installation directories.
|
||||
* Could be empty if the {@link ApplicationLayout} does not support application Extensions.
|
||||
|
@ -92,12 +92,12 @@ public class ModuleUtilities {
|
||||
if (!rootDir.exists() || remainingDepth <= 0) {
|
||||
return moduleRootDirs;
|
||||
}
|
||||
|
||||
ResourceFile[] subDirs = rootDir.listFiles(ResourceFile::isDirectory);
|
||||
|
||||
ResourceFile[] subDirs = rootDir.listFiles(ResourceFile::isDirectory);
|
||||
if (subDirs == null) {
|
||||
throw new RuntimeException("Failed to read directory: " + rootDir);
|
||||
}
|
||||
|
||||
|
||||
for (ResourceFile subDir : subDirs) {
|
||||
if ("build".equals(subDir.getName())) {
|
||||
continue; // ignore all "build" directories
|
||||
@ -230,9 +230,9 @@ public class ModuleUtilities {
|
||||
* @param modules The modules to get the library directories of.
|
||||
* @return A collection of library directories from the given modules.
|
||||
*/
|
||||
public static Collection<ResourceFile> getModuleLibDirectories(Map<String, GModule> modules) {
|
||||
public static Collection<ResourceFile> getModuleLibDirectories(Collection<GModule> modules) {
|
||||
List<ResourceFile> libraryDirectories = new ArrayList<>();
|
||||
for (GModule module : modules.values()) {
|
||||
for (GModule module : modules) {
|
||||
module.collectExistingModuleDirs(libraryDirectories, "lib");
|
||||
module.collectExistingModuleDirs(libraryDirectories, "libs");
|
||||
}
|
||||
@ -245,10 +245,10 @@ public class ModuleUtilities {
|
||||
* @param modules The modules to get the compiled .class and resources directories of.
|
||||
* @return A collection of directories containing classes and resources from the given modules.
|
||||
*/
|
||||
public static Collection<ResourceFile> getModuleBinDirectories(Map<String, GModule> modules) {
|
||||
public static Collection<ResourceFile> getModuleBinDirectories(Collection<GModule> modules) {
|
||||
String[] binaryPathTokens = BINARY_PATH.split(":");
|
||||
List<ResourceFile> binDirectories = new ArrayList<>();
|
||||
for (GModule module : modules.values()) {
|
||||
for (GModule module : modules) {
|
||||
Arrays.stream(binaryPathTokens)
|
||||
.forEach(token -> module.collectExistingModuleDirs(binDirectories, token));
|
||||
}
|
||||
@ -404,4 +404,31 @@ public class ModuleUtilities {
|
||||
.map(dir -> dir.getParentFile().getFile(false))
|
||||
.anyMatch(dir -> FileUtilities.isPathContainedWithin(dir, moduleRootDir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given module has been uninstalled.
|
||||
* @param path the module path to check
|
||||
* @return true if uninstalled
|
||||
*/
|
||||
public static boolean isUninstalled(String path) {
|
||||
return isUninstalled(new File(path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given module has been uninstalled.
|
||||
* @param dir the module dir to check
|
||||
* @return true if uninstalled
|
||||
*/
|
||||
public static boolean isUninstalled(File dir) {
|
||||
return new File(dir, MANIFEST_FILE_NAME_UNINSTALLED).exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given module has been uninstalled.
|
||||
* @param dir the module dir to check
|
||||
* @return true if uninstalled
|
||||
*/
|
||||
public static boolean isUninstalled(ResourceFile dir) {
|
||||
return new ResourceFile(dir, MANIFEST_FILE_NAME_UNINSTALLED).exists();
|
||||
}
|
||||
}
|
||||
|
@ -90,10 +90,10 @@ task zipSource (type: Zip) {
|
||||
|
||||
task buildExtension (type: Zip) {
|
||||
|
||||
archiveBaseName = "${ZIP_NAME_PREFIX}_${project.name}"
|
||||
archiveExtension = 'zip'
|
||||
destinationDirectory = DISTRIBUTION_DIR
|
||||
archiveVersion = ''
|
||||
def archiveBaseName = "${ZIP_NAME_PREFIX}_${project.name}"
|
||||
def archiveExtension = 'zip'
|
||||
def destinationDirectory = DISTRIBUTION_DIR
|
||||
def archiveVersion = ''
|
||||
|
||||
// Make sure that we don't try to copy the same file with the same path into the
|
||||
// zip (this can happen!)
|
||||
|
@ -41,6 +41,7 @@ import ghidra.framework.main.*;
|
||||
import ghidra.framework.model.*;
|
||||
import ghidra.framework.plugintool.dialog.*;
|
||||
import ghidra.framework.preferences.Preferences;
|
||||
import ghidra.framework.project.extensions.*;
|
||||
import ghidra.framework.remote.User;
|
||||
import ghidra.framework.store.LockException;
|
||||
import ghidra.program.database.ProgramContentHandler;
|
||||
|
@ -609,14 +609,14 @@ Before you can do anything else, you must first create a project. Projects are u
|
||||
</ul>
|
||||
<li>Extra Windows are located in the Windows menu</li>
|
||||
<li>The CodeBrowser tool also consists of preconfigured plugins.</li>
|
||||
<li>Users can add additional actions with the File->Configure... action</li>
|
||||
<li>Users can add additional actions with the File->Configure action</li>
|
||||
</ul>
|
||||
<div role="note">
|
||||
<p>
|
||||
<b><u>Notes:</u></b>
|
||||
<ul>
|
||||
<li>The CodeBrowser is the default tool, created by configuring Ghidra plugins in a way that makes it useful for disassembling, navigating, and documenting assembly code. It has been configured to provide all the basic functionality needed to reverse-engineer a program.</li>
|
||||
<li>To configure more plugins into a particular tool, such as the CodeBrowser, use the <b>File->Configure...</b> action.</li>
|
||||
<li>To configure more plugins into a particular tool, such as the CodeBrowser, use the <b>File->Configure</b> action.</li>
|
||||
<ul>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -409,7 +409,7 @@ can be found in the <i><GhidraInstallDir></i>/Extensions directory.</p>
|
||||
<p>Ghidra extensions are designed to be installed and uninstalled from the Ghidra front-end GUI:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Click <b>File → Install Extensions...</b></li>
|
||||
<li>Click <b>File → Install Extensions</b></li>
|
||||
<li>Check boxes to install extensions; uncheck boxes to uninstall extensions</li>
|
||||
<li>Restart Ghidra for the changes to take effect</li>
|
||||
</ol>
|
||||
|
Loading…
Reference in New Issue
Block a user