GP-3569 - Cleanup of Extension management

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

View File

@ -53,7 +53,7 @@
<P>The default tool is pre-configured with a collection of plugins relevant for the Listing and
for Debugger-related operations. As always, there is some chance that the tool will launch with
some portion of the plugins not displayed or with a less-than-optimal layout. To verify which
plugins you have, you can select <SPAN class="menu">File &rarr; Configure...</SPAN>. "Debugger"
plugins you have, you can select <SPAN class="menu">File &rarr; Configure</SPAN>. "Debugger"
should already be selected. Choosing "Configure All Plugins" (the plug icon near the top
right), should show the full list of pre-selected plugins. Debugger-related plugins all begin
with "Debugger". At a bare minimum, you will need the "DebuggerTargetsPlugin" and the

View File

@ -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.

View File

@ -1,71 +1,156 @@
<!doctype HTML public "-//W3C//DTD HTML 4.0 Frameset//EN">
<html>
<head>
<title>Extension Installation</title>
<meta http-equiv="content-type" content="text/html; charset=windows-1252">
<link rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</head>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<body>
<HTML>
<HEAD>
<TITLE>Extension Installation</TITLE>
<META http-equiv="content-type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<h1><a name="Extensions"></a>
Ghidra Extensions</h1>
<p>Ghidra Extensions (formerly 'contribs') are Ghidra software modules that are included with a Ghidra release but not
installed by default. Ghidra Extensions can be installed and uninstalled by Ghidra at runtime, with the changes taking
effect when Ghidra is restarted. This dialog can be opened by selecting the <b>Extensions</b>
option on the project file menu.</p>
<BODY>
<H1><A name="Extensions"></A> Ghidra Extensions</H1>
<p>
<center>
<table border="0" width="100%">
<tr>
<td width="100%" align="center"><img border="0" src="images/ConfigureExtensions.png"></td>
</tr>
</table>
</center>
</p>
<P>Ghidra Extensions are Ghidra software modules that can be installed
into a Ghidra distribution. This allows users to create and share new plugins and scripts.
Ghidra ships with some pre-built extensions that not installed by default.
</P>
<P>Ghidra Extensions can be installed and uninstalled at runtime, with the changes taking effect
when Ghidra is restarted. The extension installation dialog can
be opened by selecting the <B>Install Extensions</B> option on the project <B>File</B> menu.</P>
<h2>Dialog Components</h2>
<h3>Extensions List</h3>
<blockquote>
The list of extensions is populated when the dialog is launched. To build the list, Ghidra looks in several locations:
<ul>
<li>Extension Installation Directories: Contains any extensions that have been installed. The directories are located at:</li>
<ul>
<li><i>[user dir]/.ghidra/.ghidra_[version]/Extensions</i> - Installed/uninstalled from this dialog</li>
<li><i>[installation dir]/Ghidra/Extensions/</i> - Installed/uninstalled from filesystem manually</li>
</ul>
<li>Extensions Archive Directory: This is where all archive files (zips) are stored. It is located at <i>[installation dir]/Extensions/Ghidra/</i></li>
</ul>
<p><b>Note: </b> Extensions that have been installed directly into the Ghidra installation directory cannot be uninstalled
from this dialog. They must be manually removed from the filesystem.</p>
</blockquote>
<BLOCKQUOTE>
<CENTER>
<TABLE border="0" width="100%">
<TR>
<TD width="100%" align="center"><IMG alt="" border="0" src=
"images/ConfigureExtensions.png"></TD>
</TR>
</TABLE>
</CENTER>
<BR>
<BR>
</BLOCKQUOTE>
<h3>Description Panel</h3>
<blockquote>
Displays metadata about the extension selected in the Extensions List. The information displayed is extracted from the <i>extensions.properties</i> file associated
with the extension.
<H2>Dialog Components</H2>
<h4><img border="0" src="images/program_obj.png">extension.properties</h4>
<p>The existence of this file is what tells Ghidra that the folder or zip file is a Ghidra Extension. It is a simple property file that can contain the following 4 attributes:</p>
<ul>
<li><b>name</b>: Human-readable name of the extension. This is what will be displayed in the dialog.</li>
<li><b>desc</b>: Brief description of the extension.</li>
<li><b>author</b>: Creator of the extension.</li>
<li><b>createdOn</b>: Date of extension creation, in the format mm/dd/yyyy</li>
</ul>
</blockquote>
<H3>Extensions Table</H3>
<h3><a name="ExtensionTools"></a>Tools Panel</h3>
<blockquote>
<ul>
<li>&nbsp;<img border="0" src="images/Plus.png">&nbsp; Allows the user to install a new extension. An extension can be any folder or zip file that contains an <i>extensions.properties</i> file.
When one of these is selected, it will be copied to the extension installation folder and extracted (if it is a zip).
<li>&nbsp;<img border="0" src="Icons.REFRESH_ICON">&nbsp; Reloads the Extensions List
</ul>
</blockquote>
<BLOCKQUOTE>
<P>The list of extensions is populated when the dialog is launched. To build the list, Ghidra
looks in several locations:</P>
<p class="relatedtopic">Related Topics:</p>
</body>
</html>
<UL>
<LI>Extension Installation Directories: Contains any extensions that have been installed.
The directories are located at:</LI>
<LI style="list-style: none">
<UL>
<LI><I>[user dir]/.ghidra/.ghidra_[version]/Extensions</I> - Installed/uninstalled from
this dialog</LI>
<LI><I>[installation dir]/Ghidra/Extensions/</I> - Installed/uninstalled from
filesystem manually</LI>
</UL>
</LI>
<LI>Extensions Archive Directory: This is where archive files (zips) that are bundled with
the distribution are stored. It is
located at <I>[installation dir]/Extensions/Ghidra/</I>. This directory is not intended for
end-user extensions.
</LI>
</UL>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0">The color red is used in the table
to indicate that the extension version does not match the Ghidra version.</P>
</BLOCKQUOTE>
<P><B>Note:</B> Extensions that have been installed directly into the Ghidra installation
directory cannot be uninstalled from this dialog. They must be manually removed from the
filesystem.</P>
</BLOCKQUOTE>
<H3>Description Panel</H3>
<BLOCKQUOTE>
<P>Displays metadata about the extension selected in the Extensions List. The information
displayed is extracted from the <CODE><I>extensions.properties</I></CODE> file associated with the
extension.</P>
<P>The existence of this file is what tells Ghidra that the folder or zip file is a Ghidra
Extension. It is a simple property file that can contain the following attributes:</P>
<UL>
<LI><B>name</B>: Human-readable name of the extension. This is what will be displayed in
the dialog.</LI>
<LI><B>description</B>: Brief description of the extension.</LI>
<LI><B>author</B>: Creator of the extension.</LI>
<LI><B>createdOn</B>: Date of extension creation, in the format mm/dd/yyyy.</LI>
<LI><B>version</B>: The version of Ghidra for which this extension was built.</LI>
</UL>
</BLOCKQUOTE>
<H3><A name="ExtensionTools"></A>Tools Panel</H3>
<BLOCKQUOTE>
<UL>
<LI><IMG alt="" border="0" src="images/Plus.png">&nbsp; Allows the user to install a
new extension. An extension can be any folder or zip file that contains an
<I>extensions.properties</I> file. When one of these is selected, it will be copied to the
extension installation folder and extracted (if it is a zip).</LI>
<LI>&nbsp;<IMG alt="" border="0" src="Icons.REFRESH_ICON">&nbsp; Reloads the Extensions
List</LI>
</UL>
</BLOCKQUOTE>
<H2>Building Extensions</H2>
<BLOCKQUOTE>
<P>
An extension is simply a Ghidra module that contains an <CODE>extension.properties</CODE> file.
Building an extension is very similar to building a ghidra module, which is done by using
<CODE>gradle</CODE>.
</P>
<P>
Ghidra includes a <CODE>Skeleton</CODE> module in the distribution that is meant to be used as
a template when creating extensions. This module can be found at
</P>
<BLOCKQUOTE>
<P>
<CODE CLASS="path">&lt;GHIDRA_INSTALL_DIR&gt;/Extensions/Ghidra</CODE>
</P>
</BLOCKQUOTE>
<P>
Copy and rename this directory to get started writing your own module. You can then use
<CODE>gradle</CODE> to build the extension by running this command from within your extension
directory:
</P>
<BLOCKQUOTE>
<P>
<CODE CLASS="path">gradle -PGHIDRA_INSTALL_DIR=/path/to/ghidra/ghidra_&lt;version&gt;/ buildExtension</CODE>
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<BR>
<BR>
<BR>
<P class="relatedtopic">Related Topics:</P>
<UL>
<LI><A href="help/topics/Tool/Configure_Tool.htm">Configuring Tool Plugins</A></LI>
</UL>
<BR>
<BR>
<BR>
</BODY>
</HTML>

View File

@ -19,7 +19,7 @@
<P>This plugin doesn't perform any natural language translation by itself. The
user must install <b>string translation service</b>s that do the actual translation.
Extensions to Ghidra are installed via the <b>File
<IMG src="help/shared/arrow.gif" alt="-&gt;" border="0"> <a href="../FrontEndPlugin/Extensions.htm">Install Extensions...</a></b>
<IMG src="help/shared/arrow.gif" alt="-&gt;" border="0"> <a href="../FrontEndPlugin/Extensions.htm">Install Extensions</a></b>
menu.</P>
<P>When a string has been translated, the translated value will be shown in place of

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
}

View File

@ -43,7 +43,7 @@
a Code Browser by selecting the
</p>
<div class="informalexample">
<span class="bold"><strong>File -&gt; Configure...</strong></span>
<span class="bold"><strong>File -&gt; Configure</strong></span>
</div>
<p>
menu option, then clicking on the <span class="emphasis"><em>Configure</em></span> link under the

View File

@ -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]);

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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());
}
/**

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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"/>

View File

@ -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"/>

View File

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

View File

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

View File

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

View File

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

View File

@ -50,11 +50,11 @@ import ghidra.framework.main.AppInfo;
import ghidra.framework.main.UserAgreementDialog;
import ghidra.framework.model.*;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.dialog.ExtensionTableProvider;
import ghidra.framework.plugintool.dialog.ManagePluginsDialog;
import ghidra.framework.plugintool.mgr.*;
import ghidra.framework.plugintool.util.*;
import ghidra.framework.project.ProjectDataService;
import ghidra.framework.project.extensions.ExtensionTableProvider;
import ghidra.util.*;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
@ -198,7 +198,7 @@ public abstract class PluginTool extends AbstractDockingTool {
return new DefaultPluginsConfiguration();
}
protected PluginsConfiguration getPluginsConfiguration() {
public PluginsConfiguration getPluginsConfiguration() {
return pluginMgr.getPluginsConfiguration();
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,208 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.plugintool.dialog;
import java.io.File;
import ghidra.framework.Application;
import utility.module.ModuleUtilities;
/**
* Representation of a Ghidra extension. This class encapsulates all information required to
* uniquely identify an extension and where (or if) it has been installed.
* <p>
* Note that hashCode and equals have been implemented for this. Two extension
* descriptions are considered equal if they have the same {@link #name} attribute; all other
* fields are unimportant save for display purposes.
*
*/
public class ExtensionDetails implements Comparable<ExtensionDetails> {
/** Absolute path to where this extension is installed. If not installed, this will be null. */
private String installPath;
/**
* Absolute path to where the original source archive (zip) for this extension can be found. If
* there is no archive (likely because this is an extension that comes pre-installed with
* Ghidra, or Ghidra is being run in development mode), this will be null.
*/
private String archivePath;
/** Name of the extension. This must be unique.*/
private String name;
/** Brief description, for display purposes only.*/
private String description;
/** Date when the extension was created, for display purposes only.*/
private String createdOn;
/** Author of the extension, for display purposes only.*/
private String author;
/** The extension version */
private String version;
/**
* Constructor.
*
* @param name unique name of the extension; cannot be null
* @param description brief explanation of what the extension does; can be null
* @param author creator of the extension; can be null
* @param createdOn creation date of the extension, can be null
* @param version the extension version
*/
public ExtensionDetails(String name, String description, String author, String createdOn,
String version) {
this.name = name;
this.description = description;
this.author = author;
this.createdOn = createdOn;
this.version = version;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ExtensionDetails other = (ExtensionDetails) obj;
if (name == null) {
if (other.name != null) {
return false;
}
}
else if (!name.equals(other.name)) {
return false;
}
return true;
}
/**
* Returns the location where this extension is installed. If the extension is
* not installed this will be null.
*
* @return the extension path, or null
*/
public String getInstallPath() {
return installPath;
}
public void setInstallPath(String path) {
this.installPath = path;
}
/**
* Returns the location where the extension archive is located. If there is no
* archive this will be null.
*
* @return the archive path, or null
*/
public String getArchivePath() {
return archivePath;
}
public void setArchivePath(String path) {
this.archivePath = path;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getCreatedOn() {
return createdOn;
}
public void setCreatedOn(String date) {
this.createdOn = date;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
/**
* An extension is known to be installed if it has a valid installation path AND that path
* contains a Module.manifest file.
* <p>
* Note: The module manifest file is a marker that indicates several things; one of which is
* the installation status of an extension. When a user marks an extension to be uninstalled (by
* checking the appropriate checkbox in the {@link ExtensionTableModel}), the only thing
* that is done is to remove this manifest file, which tells the {@link ExtensionTableProvider}
* to remove the entire extension directory on the next launch.
*
* @return true if the extension is installed.
*/
public boolean isInstalled() {
if (installPath == null || installPath.isEmpty()) {
return false;
}
// If running out of a jar and the install path is valid, just return true. The alternative
// would be to inspect the jar and verify that the install path is there and is valid, but that's
// overkill.
if (Application.inSingleJarMode()) {
return true;
}
File mm = new File(installPath, ModuleUtilities.MANIFEST_FILE_NAME);
return mm.exists();
}
@Override
public int compareTo(ExtensionDetails other) {
return name.compareTo(other.name);
}
}

View File

@ -1,70 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.plugintool.dialog;
import java.io.File;
import ghidra.util.exception.UsrException;
/**
* Defines an exception that can be thrown by {@link ExtensionUtils}. This is intended to provide
* detailed information about issues that arise during installation (or removal) of
* Extensions.
*
*/
public class ExtensionException extends UsrException {
/** Provides more detail as to the specific source of the exception. */
public enum ExtensionExceptionType {
/** Thrown if the required installation location does not exist */
INVALID_INSTALL_LOCATION,
/** Thrown when installing an extension to an existing location */
DUPLICATE_FILE_ERROR,
/** Thrown when there is a problem reading/extracting a zip file during installation */
ZIP_ERROR,
/** Thrown when there is a problem copying a folder during an installation */
COPY_ERROR,
/** Thrown when the user cancels the installation */
INSTALL_CANCELLED
}
private ExtensionExceptionType exceptionType;
private File errorFile = null; // If there's a file relevant to the exception, populate this.
public ExtensionException(String msg, ExtensionExceptionType exceptionType) {
super(msg);
this.exceptionType = exceptionType;
}
public ExtensionException(String msg, ExtensionExceptionType exceptionType, File errorFile) {
super(msg);
this.errorFile = errorFile;
this.exceptionType = exceptionType;
}
public ExtensionExceptionType getExceptionType() {
return exceptionType;
}
public File getErrorFile() {
return errorFile;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,953 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.project.extensions;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import docking.widgets.OkDialog;
import docking.widgets.OptionDialog;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskLauncher;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
/**
* Utility class for managing Ghidra Extensions.
* <p>
* Extensions are defined as any archive or folder that contains an <code>extension.properties</code>
* file. This properties file can contain the following attributes:
* <ul>
* <li>name (required)</li>
* <li>description</li>
* <li>author</li>
* <li>createdOn (format: MM/dd/yyyy)</li>
* <li>version</li>
* </ul>
*
* <p>
* Extensions may be installed/uninstalled by users at runtime, using the
* {@link ExtensionTableProvider}. Installation consists of unzipping the extension archive to an
* installation folder, currently <code>{ghidra user settings dir}/Extensions</code>. To uninstall,
* the unpacked folder is simply removed.
*/
public class ExtensionUtils {
/** Magic number that identifies the first bytes of a ZIP archive. This is used to verify
that a file is a zip rather than just checking the extension. */
private static final int ZIPFILE = 0x504b0304;
public static String PROPERTIES_FILE_NAME = "extension.properties";
public static String PROPERTIES_FILE_NAME_UNINSTALLED = "extension.properties.uninstalled";
private static final Logger log = LogManager.getLogger(ExtensionUtils.class);
/**
* Performs extension maintenance. This should be called at startup, before any plugins or
* extension points are loaded.
*/
public static void initializeExtensions() {
Extensions extensions = getAllInstalledExtensions();
// delete any extensions marked for removal
extensions.cleanupExtensionsMarkedForRemoval();
// check for duplicates in the remaining extensions
extensions.reportDuplicateExtensions();
}
/**
* Gets all known extensions that have not been marked for removal.
*
* @return set of installed extensions
*/
public static Set<ExtensionDetails> getActiveInstalledExtensions() {
Extensions extensions = getAllInstalledExtensions();
return extensions.getActiveExtensions();
}
/**
* Returns all installed extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionInstallationDirs}.
*
* @return set of installed extensions
*/
public static Set<ExtensionDetails> getInstalledExtensions() {
Extensions extensions = getAllInstalledExtensions();
return extensions.get();
}
private static Extensions getAllInstalledExtensions() {
Extensions extensions = new Extensions();
// Find all extension.properties or extension.properties.uninstalled files in
// the install directory and create a ExtensionDetails object for each.
ApplicationLayout layout = Application.getApplicationLayout();
for (ResourceFile installDir : layout.getExtensionInstallationDirs()) {
if (!installDir.isDirectory()) {
continue;
}
log.trace("Checking extension installation dir '" + installDir);
File dir = installDir.getFile(false);
List<File> propFiles = findExtensionPropertyFiles(dir);
for (File propFile : propFiles) {
ExtensionDetails extension = createExtensionFromProperties(propFile);
if (extension == null) {
continue;
}
// We found this extension in the installation directory, so set the install path
// property and add to the final set.
File extInstallDir = propFile.getParentFile();
extension.setInstallDir(extInstallDir);
log.trace("Loading extension '" + extension.getName() + "' from: " + extInstallDir);
extensions.add(extension);
}
}
return extensions;
}
/**
* Returns all archive extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionArchiveDir}. This are added to an installation as
* part of the build processes.
* <p>
* Archived extensions may be zip files and directories.
*
* @return set of archive extensions
*/
public static Set<ExtensionDetails> getArchiveExtensions() {
log.trace("Finding archived extensions");
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile archiveDir = layout.getExtensionArchiveDir();
if (archiveDir == null) {
log.trace("No extension archive dir found");
return Collections.emptySet();
}
ResourceFile[] archiveFiles = archiveDir.listFiles();
if (archiveFiles == null) {
log.trace("No files in extension archive dir: " + archiveDir);
return Collections.emptySet(); // no files or dirs inside of the archive directory
}
Set<ExtensionDetails> extensions = new HashSet<>();
findExtensionsInZips(archiveFiles, extensions);
findExtensionsInFolder(archiveDir.getFile(false), extensions);
return extensions;
}
private static void findExtensionsInZips(ResourceFile[] archiveFiles,
Set<ExtensionDetails> extensions) {
for (ResourceFile file : archiveFiles) {
ExtensionDetails extension = createExtensionDetailsFromArchive(file);
if (extension == null) {
log.trace("Skipping archive file; not an extension: " + file);
continue;
}
if (extensions.contains(extension)) {
log.error(
"Skipping extension \"" + extension.getName() + "\" found at " +
extension.getInstallPath() +
".\nArchived extension by that name already found.");
}
extensions.add(extension);
}
}
private static void findExtensionsInFolder(File dir, Set<ExtensionDetails> extensions) {
List<File> propFiles = findExtensionPropertyFiles(dir);
for (File propFile : propFiles) {
ExtensionDetails extension = createExtensionFromProperties(propFile);
if (extension == null) {
continue;
}
// We found this extension in the installation directory, so set the archive path
// property and add to the final set.
File extDir = propFile.getParentFile();
extension.setArchivePath(extDir.getAbsolutePath());
if (extensions.contains(extension)) {
log.error(
"Skipping duplicate extension \"" + extension.getName() + "\" found at " +
extension.getInstallPath());
}
extensions.add(extension);
}
}
/**
* Installs the given extension file. This can be either an archive (zip) or a directory that
* contains an extension.properties file.
*
* @param file the extension to install
* @return true if the extension was successfully installed
*/
public static boolean install(File file) {
log.trace("installing file " + file);
if (file == null) {
log.error("Install file cannot be null");
return false;
}
ExtensionDetails extension = getExtension(file, false);
if (extension == null) {
Msg.showError(ExtensionUtils.class, null, "Error Installing Extension",
file.getAbsolutePath() + " does not point to a valid ghidra extension");
return false;
}
if (checkForConflictWithDevelopmentExtension(extension)) {
return false;
}
if (checkForDuplicateExtensions(extension)) {
return false;
}
// Verify that the version of the extension is valid for this version of Ghidra. If not,
// just exit without installing.
if (!validateExtensionVersion(extension)) {
return false;
}
AtomicBoolean installed = new AtomicBoolean(false);
TaskLauncher.launchModal("Installing Extension", (monitor) -> {
installed.set(doRunInstallTask(extension, file, monitor));
});
boolean success = installed.get();
if (success) {
log.trace("Finished installing " + file);
}
else {
log.trace("Failed to install " + file);
}
return success;
}
private static boolean doRunInstallTask(ExtensionDetails extension, File file,
TaskMonitor monitor) {
try {
if (file.isFile()) {
return unzipToInstallationFolder(extension, file, monitor);
}
return copyToInstallationFolder(file, monitor);
}
catch (CancelledException e) {
log.info("Extension installation cancelled by user");
}
catch (IOException e) {
Msg.showError(ExtensionUtils.class, null, "Error Installing Extension",
"Unexpected error installing extension", e);
}
return false;
}
/**
* Installs the given extension from its declared archive path
* @param extension the extension
* @return true if successful
*/
public static boolean installExtensionFromArchive(ExtensionDetails extension) {
if (extension == null) {
log.error("Extension to install cannot be null");
return false;
}
String archivePath = extension.getArchivePath();
if (archivePath == null) {
log.error(
"Cannot install from archive; extension is missing archive path");
return false;
}
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile extInstallDir = layout.getExtensionInstallationDirs().get(0);
String extName = extension.getName();
File extDestinationDir = new ResourceFile(extInstallDir, extName).getFile(false);
File archiveFile = new File(archivePath);
if (install(archiveFile)) {
extension.setInstallDir(new File(extDestinationDir, extName));
return true;
}
return false;
}
/**
* Compares the given extension version to the current Ghidra version. If they are different,
* then the user will be prompted to confirm the installation. This method will return true
* if the versions match or the user has chosen to install anyway.
*
* @param extension the extension
* @return true if the versions match or the user has chosen to install anyway
*/
private static boolean validateExtensionVersion(ExtensionDetails extension) {
String extVersion = extension.getVersion();
if (extVersion == null) {
extVersion = "<no version>";
}
String appVersion = Application.getApplicationVersion();
if (extVersion.equals(appVersion)) {
return true;
}
String message = "Extension version mismatch.\nName: " + extension.getName() +
"Extension version: " + extVersion + ".\nGhidra version: " + appVersion + ".";
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
"Extension Version Mismatch",
message,
"Install Anyway");
if (choice != OptionDialog.OPTION_ONE) {
log.info(removeNewlines(message + " Did not install"));
return false;
}
return true;
}
private static String removeNewlines(String s) {
return s.replaceAll("\n", " ");
}
private static boolean checkForDuplicateExtensions(ExtensionDetails newExtension) {
Extensions extensions = getAllInstalledExtensions();
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
return false;
}
log.trace("Duplicate extensions found by name '" + newExtension.getName() + "'");
if (matches.size() > 1) {
reportMultipleDuplicateExtensionsWhenInstalling(newExtension, matches);
return true;
}
ExtensionDetails installedExtension = matches.get(0);
String message =
"Attempting to install an extension matching the name of an existing extension.\n" +
"New extension version: " + newExtension.getVersion() + ".\n" +
"Installed extension version: " + installedExtension.getVersion() + ".\n\n" +
"To install, click 'Remove Existing', restart Ghidra, then install again.";
int choice = OptionDialog.showOptionDialogWithCancelAsDefaultButton(null,
"Duplicate Extension",
message,
"Remove Existing");
String installPath = installedExtension.getInstallPath();
if (choice != OptionDialog.OPTION_ONE) {
log.info(
removeNewlines(
message + " Skipping installation. Original extension still installed: " +
installPath));
return true;
}
//
// At this point the user would like to replace the existing extension. We cannot delete
// the existing extension, as it may be in use; mark it for removal.
//
log.info(
removeNewlines(
message + " Installing new extension. Existing extension will be removed after " +
"restart: " + installPath));
installedExtension.markForUninstall();
return true;
}
private static void reportMultipleDuplicateExtensionsWhenInstalling(ExtensionDetails extension,
List<ExtensionDetails> matches) {
StringBuilder buffy = new StringBuilder();
buffy.append("Found multiple duplicate extensions while trying to install '")
.append(extension.getName())
.append("'\n");
for (ExtensionDetails otherExtension : matches) {
buffy.append("Duplicate: " + otherExtension.getInstallPath()).append('\n');
}
buffy.append("Please close Ghidra and manually remove from these extensions from the " +
"filesystem.");
Msg.showInfo(ExtensionUtils.class, null, "Duplicate Extensions Found", buffy.toString());
}
private static void reportDuplicateExtensionsWhenLoading(String name,
List<ExtensionDetails> extensions) {
ExtensionDetails loadedExtension = extensions.get(0);
File loadedInstallDir = loadedExtension.getInstallDir();
for (int i = 1; i < extensions.size(); i++) {
ExtensionDetails duplicate = extensions.get(i);
log.info("Duplicate extension found '" + name + "'. Keeping extension from " +
loadedInstallDir + ". Skipping extension found at " +
duplicate.getInstallDir());
}
}
private static boolean checkForConflictWithDevelopmentExtension(ExtensionDetails newExtension) {
Extensions extensions = getAllInstalledExtensions();
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
return false;
}
for (ExtensionDetails extension : matches) {
if (extension.isInstalledInInstallationFolder()) {
OkDialog.showError("Duplicate Extensions Found",
"Attempting to install an extension that conflicts with an extension located in " +
"the Ghidra installation folder.\nYou must manually remove the existing " +
"extension to install the new extension.\nExisting extension: " +
extension.getInstallDir());
return true;
}
}
return false;
}
/**
* Returns true if the given file or directory is a valid ghidra extension.
* <p>
* Note: This means that the zip or directory contains an extension.properties file.
*
* @param file the zip or directory to inspect
* @return true if the given file represents a valid extension
*/
public static boolean isExtension(File file) {
return getExtension(file, true) != null;
}
private static ExtensionDetails getExtension(File file, boolean quiet) {
if (file == null) {
log.error("Cannot get an extension; null file");
return null;
}
try {
return tryToGetExtension(file);
}
catch (IOException e) {
if (quiet) {
log.trace("Exception trying to read an extension from " + file, e);
}
else {
log.error("Exception trying to read an extension from " + file, e);
}
}
return null;
}
private static ExtensionDetails tryToGetExtension(File file) throws IOException {
if (file == null) {
log.error("Cannot get an extension; null file");
return null;
}
if (file.isDirectory() && file.canRead()) {
File[] files = file.listFiles(f -> f.getName().equals(PROPERTIES_FILE_NAME));
if (files != null && files.length == 1) {
return tryToLoadExtensionFromProperties(files[0]);
}
}
// If the given file is a zip, it's an extension if there's an extension.properties
// file at the TOP LEVEL ONLY; we don't want to search for nested property files (this
// would cause us to match things like the main ghidra distribution zip file.
// eg: DatabaseTools/extension.properties is valid
// DatabaseTools/foo/extension.properties is not.
if (isZip(file)) {
try (ZipFile zipFile = new ZipFile(file)) {
Properties props = getProperties(zipFile);
if (props != null) {
return createExtensionDetails(props);
}
throw new IOException("No extension.properties file found in zip");
}
}
return null;
}
/**
* Returns true if the given file is a valid .zip archive.
*
* @param file the file to test
* @return true if file is a valid zip
*/
private static boolean isZip(File file) {
if (file == null) {
log.error("Cannot check for extension zip; null file");
return false;
}
if (file.isDirectory()) {
return false;
}
if (file.length() < 4) {
return false;
}
try (DataInputStream in =
new DataInputStream(new BufferedInputStream(new FileInputStream(file)))) {
int test = in.readInt();
return test == ZIPFILE;
}
catch (IOException e) {
log.trace("Unable to check if file is a zip file: " + file + ". " + e.getMessage());
return false;
}
}
/**
* Returns a list of files representing all the <code>extension.properties</code> files found
* under a given directory. This will only search the immediate children of the given directory.
* <p>
* Searching the child directories of a directory allows clients to pick an extension parent
* directory that contains multiple extension directories.
*
* @param installDir the directory that contains extension subdirectories
* @return list of extension.properties files
*/
private static List<File> findExtensionPropertyFiles(File installDir) {
List<File> results = new ArrayList<>();
FileUtilities.forEachFile(installDir, f -> {
if (!f.isDirectory() || f.getName().equals("Skeleton")) {
return;
}
File pf = getPropertyFile(f);
if (pf != null) {
results.add(pf);
}
});
return results;
}
/**
* Returns an extension.properties or extension.properties.uninstalled file if the given
* directory contains one.
*
* @param dir the directory to search
* @return the file, or null if doesn't exist
*/
private static File getPropertyFile(File dir) {
File f = new File(dir, PROPERTIES_FILE_NAME_UNINSTALLED);
if (f.exists()) {
return f;
}
f = new File(dir, PROPERTIES_FILE_NAME);
if (f.exists()) {
return f;
}
return null;
}
private static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
File file = resourceFile.getFile(false);
if (!isZip(file)) {
return null;
}
try (ZipFile zipFile = new ZipFile(file)) {
Properties props = getProperties(zipFile);
if (props != null) {
ExtensionDetails extension = createExtensionDetails(props);
extension.setArchivePath(file.getAbsolutePath());
return extension;
}
}
catch (IOException e) {
log.error(
"Unable to read zip file to get extension properties: " + file, e);
}
return null;
}
private static Properties getProperties(ZipFile zipFile) throws IOException {
Properties props = null;
Enumeration<ZipArchiveEntry> zipEntries = zipFile.getEntries();
while (zipEntries.hasMoreElements()) {
ZipArchiveEntry entry = zipEntries.nextElement();
Properties nextProperties = getProperties(zipFile, entry);
if (nextProperties != null) {
if (props != null) {
throw new IOException(
"Zip file contains multiple extension properties files");
}
props = nextProperties;
}
}
return props;
}
private static Properties getProperties(ZipFile zipFile, ZipArchiveEntry entry)
throws IOException {
// We only search for the property file at the top level
String path = entry.getName();
List<String> parts = FileUtilities.pathToParts(path);
if (parts.size() != 2) { // require 2 parts: dir name / props file
return null;
}
if (!entry.getName().endsWith(PROPERTIES_FILE_NAME)) {
return null;
}
InputStream propFile = zipFile.getInputStream(entry);
Properties prop = new Properties();
prop.load(propFile);
return prop;
}
/**
* Copies the given folder to the extension install location. Any existing folder at that
* location will be deleted.
* <p>
* Note: Any existing folder with the same name will be overwritten.
*
* @param sourceFolder the extension folder
* @param monitor the task monitor
* @return true if successful
* @throws IOException if the delete or copy fails
* @throws CancelledException if the user cancels the copy
*/
private static boolean copyToInstallationFolder(File sourceFolder, TaskMonitor monitor)
throws IOException, CancelledException {
log.trace("Copying extension from " + sourceFolder);
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile installDir = layout.getExtensionInstallationDirs().get(0);
File installDirRoot = installDir.getFile(false);
File newDir = new File(installDirRoot, sourceFolder.getName());
if (hasExistingExtension(newDir, monitor)) {
return false;
}
FileUtilities.copyDir(sourceFolder, newDir, monitor);
return true;
}
private static boolean hasExistingExtension(File extensionFolder, TaskMonitor monitor) {
if (extensionFolder.exists()) {
Msg.showWarn(ExtensionUtils.class, null, "Duplicate Extension Folder",
"Attempting to install a new extension over an existing directory.\n" +
"Either remove the extension for that directory from the UI\n" +
"or close Ghidra and delete the directory and try installing again.\n\n" +
"Directory: " + extensionFolder);
return true;
}
return false;
}
/**
* Unpacks a given zip file to {@link ApplicationLayout#getExtensionInstallationDirs}. The
* file permissions in the original zip will be retained.
* <p>
* Note: This method uses the Apache zip files since they keep track of permissions info;
* the built-in java objects (e.g., ZipEntry) do not.
*
* @param extension the extension
* @param file the zip file to unpack
* @param monitor the task monitor
* @return true if successful
* @throws IOException if any part of the unzipping fails, or if the target location is invalid
* @throws CancelledException if the user cancels the unzip
* @throws IOException if error unzipping zip file
*/
private static boolean unzipToInstallationFolder(ExtensionDetails extension, File file,
TaskMonitor monitor)
throws CancelledException, IOException {
log.trace("Unzipping extension from " + file);
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile installDir = layout.getExtensionInstallationDirs().get(0);
File installDirRoot = installDir.getFile(false);
File destinationFolder = new File(installDirRoot, extension.getName());
if (hasExistingExtension(destinationFolder, monitor)) {
return false;
}
try (ZipFile zipFile = new ZipFile(file)) {
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
monitor.checkCancelled();
ZipArchiveEntry entry = entries.nextElement();
String filePath = installDir + File.separator + entry.getName();
File destination = new File(filePath);
if (entry.isDirectory()) {
destination.mkdirs();
}
else {
writeZipEntryToFile(zipFile, entry, destination);
}
}
}
return true;
}
private static void writeZipEntryToFile(ZipFile zFile, ZipArchiveEntry entry, File destination)
throws IOException {
try (OutputStream outputStream =
new BufferedOutputStream(new FileOutputStream(destination))) {
// Create the file at the new location...
IOUtils.copy(zFile.getInputStream(entry), outputStream);
// ...and update its permissions. But only continue if the zip was created on a unix
//platform. If not, we cannot use the posix libraries to set permissions.
if (entry.getPlatform() != ZipArchiveEntry.PLATFORM_UNIX) {
return;
}
int mode = entry.getUnixMode();
if (mode != 0) { // 0 indicates non-unix platform
Set<PosixFilePermission> perms = getPermissions(mode);
try {
Files.setPosixFilePermissions(destination.toPath(), perms);
}
catch (UnsupportedOperationException e) {
// Need to catch this, as Windows does not support the posix call. This is not
// an error, however, and should just silently fail.
}
}
}
}
private static ExtensionDetails tryToLoadExtensionFromProperties(File file) throws IOException {
Properties props = new Properties();
try (InputStream in = new FileInputStream(file.getAbsolutePath())) {
props.load(in);
return createExtensionDetails(props);
}
}
private static ExtensionDetails createExtensionFromProperties(File file) {
try {
return tryToLoadExtensionFromProperties(file);
}
catch (IOException e) {
log.error("Error loading extension properties from " + file.getAbsolutePath(), e);
return null;
}
}
private static ExtensionDetails createExtensionDetails(Properties props) {
String name = props.getProperty("name");
String desc = props.getProperty("description");
String author = props.getProperty("author");
String date = props.getProperty("createdOn");
String version = props.getProperty("version");
return new ExtensionDetails(name, desc, author, date, version);
}
/**
* Uninstalls a given extension.
*
* @param extension the extension to uninstall
* @return true if successfully uninstalled
*/
private static boolean removeExtension(ExtensionDetails extension) {
if (extension == null) {
log.error("Extension to uninstall cannot be null");
return false;
}
File installDir = extension.getInstallDir();
if (installDir == null) {
log.error("Extension installation path is not set; unable to delete files");
return false;
}
if (FileUtilities.deleteDir(installDir)) {
extension.setInstallDir(null);
return true;
}
return false;
}
/**
* Converts Unix permissions to a set of {@link PosixFilePermission}s.
*
* @param unixMode integer representation of file permissions
* @return set of POSIX file permissions
*/
private static Set<PosixFilePermission> getPermissions(int unixMode) {
Set<PosixFilePermission> permissions = new HashSet<>();
if ((unixMode & 0400) != 0) {
permissions.add(PosixFilePermission.OWNER_READ);
}
if ((unixMode & 0200) != 0) {
permissions.add(PosixFilePermission.OWNER_WRITE);
}
if ((unixMode & 0100) != 0) {
permissions.add(PosixFilePermission.OWNER_EXECUTE);
}
if ((unixMode & 0040) != 0) {
permissions.add(PosixFilePermission.GROUP_READ);
}
if ((unixMode & 0020) != 0) {
permissions.add(PosixFilePermission.GROUP_WRITE);
}
if ((unixMode & 0010) != 0) {
permissions.add(PosixFilePermission.GROUP_EXECUTE);
}
if ((unixMode & 0004) != 0) {
permissions.add(PosixFilePermission.OTHERS_READ);
}
if ((unixMode & 0002) != 0) {
permissions.add(PosixFilePermission.OTHERS_WRITE);
}
if ((unixMode & 0001) != 0) {
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
}
return permissions;
}
/**
* A collection of all extensions found. This class provides methods processing duplicates and
* managing extensions marked for removal.
*/
private static class Extensions {
private Map<String, List<ExtensionDetails>> extensionsByName = new HashMap<>();
void add(ExtensionDetails e) {
extensionsByName.computeIfAbsent(e.getName(), n -> new ArrayList<>()).add(e);
}
Set<ExtensionDetails> getActiveExtensions() {
return extensionsByName.values()
.stream()
.map(list -> list.get(0))
.filter(ext -> !ext.isPendingUninstall())
.collect(Collectors.toSet());
}
List<ExtensionDetails> getMatchingExtensions(ExtensionDetails extension) {
return extensionsByName.computeIfAbsent(extension.getName(), name -> List.of());
}
void cleanupExtensionsMarkedForRemoval() {
Set<String> names = new HashSet<>(extensionsByName.keySet());
for (String name : names) {
List<ExtensionDetails> extensions = extensionsByName.get(name);
Iterator<ExtensionDetails> it = extensions.iterator();
while (it.hasNext()) {
ExtensionDetails extension = it.next();
if (!extension.isPendingUninstall()) {
continue;
}
if (!removeExtension(extension)) {
log.error("Error removing extension: " + extension.getInstallPath());
}
it.remove();
}
if (extensions.isEmpty()) {
extensionsByName.remove(name);
}
}
}
void reportDuplicateExtensions() {
Set<Entry<String, List<ExtensionDetails>>> entries = extensionsByName.entrySet();
for (Entry<String, List<ExtensionDetails>> entry : entries) {
List<ExtensionDetails> list = entry.getValue();
if (list.size() == 1) {
continue;
}
reportDuplicateExtensionsWhenLoading(entry.getKey(), list);
}
}
/**
* Returns all unique (no duplicates) extensions that the application is aware of
* @return the extensions
*/
Set<ExtensionDetails> get() {
return extensionsByName.values()
.stream()
.map(list -> list.get(0))
.collect(Collectors.toSet());
}
}
}

View File

@ -0,0 +1,320 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.project.tool;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import org.jdom.Element;
import docking.widgets.OptionDialog;
import generic.json.Json;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.dialog.PluginInstallerDialog;
import ghidra.framework.plugintool.util.PluginDescription;
import ghidra.framework.project.extensions.ExtensionDetails;
import ghidra.framework.project.extensions.ExtensionUtils;
import ghidra.util.NumericUtilities;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.xml.XmlUtilities;
import utilities.util.FileUtilities;
/**
* A class to manage saving and restoring of known extension used by this tool.
*/
class ExtensionManager {
private static final String EXTENSION_ATTRIBUTE_NAME_ENCODED = "ENCODED_NAME";
private static final String EXTENSION_ATTRIBUTE_NAME = "NAME";
private static final String EXTENSIONS_XML_NAME = "EXTENSIONS";
private static final String EXTENSION_ELEMENT_NAME = "EXTENSION";
private PluginTool tool;
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
ExtensionManager(PluginTool tool) {
this.tool = tool;
}
void checkForNewExtensions() {
if (newExtensionPlugins.isEmpty()) {
return;
}
propmtToConfigureNewPlugins(newExtensionPlugins);
newExtensionPlugins.clear();
}
private void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
// Offer the user a chance to configure any newly discovered plugins
int option = OptionDialog.showYesNoDialog(tool.getToolFrame(), "New Plugins Found!",
"New extension plugins detected. Would you like to configure them?");
if (option == OptionDialog.YES_OPTION) {
List<PluginDescription> pluginDescriptions = getPluginDescriptions(plugins);
PluginInstallerDialog pluginInstaller = new PluginInstallerDialog("New Plugins Found!",
tool, new PluginConfigurationModel(tool), pluginDescriptions);
tool.showDialog(pluginInstaller);
}
}
void saveToXml(Element xml) {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
Element extensionsParent = new Element(EXTENSIONS_XML_NAME);
for (ExtensionDetails ext : installedExtensions) {
Element child = new Element(EXTENSION_ELEMENT_NAME);
String name = ext.getName();
if (XmlUtilities.hasInvalidXMLCharacters(name)) {
child.setAttribute(EXTENSION_ATTRIBUTE_NAME_ENCODED, NumericUtilities
.convertBytesToString(name.getBytes(StandardCharsets.UTF_8)));
}
else {
child.setAttribute(EXTENSION_ATTRIBUTE_NAME, name);
}
extensionsParent.addContent(child);
}
xml.addContent(extensionsParent);
}
void restoreFromXml(Element xml) {
Set<ExtensionDetails> installedExtensions = getExtensions();
if (installedExtensions.isEmpty()) {
return;
}
Set<String> knownExtensionNames = getKnownExtensions(xml);
Set<ExtensionDetails> newExtensions = new HashSet<>(installedExtensions);
for (ExtensionDetails ext : installedExtensions) {
if (knownExtensionNames.contains(ext.getName())) {
newExtensions.remove(ext);
}
}
// Get a list of all plugins contained in those extensions. If there are none, then either
// none of the extensions has any plugins, or Ghidra hasn't been restarted since installing
// the extension(s), so none of the plugin classes have been loaded. In either case, there
// is nothing more to do.
Set<Class<?>> newPlugins = findLoadedPlugins(newExtensions);
newExtensionPlugins.addAll(newPlugins);
}
private Set<ExtensionDetails> getExtensions() {
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
return installedExtensions.stream()
.filter(e -> !e.isInstalledInInstallationFolder())
.collect(Collectors.toSet());
}
private Set<String> getKnownExtensions(Element xml) {
Set<String> knownExtensionNames = new HashSet<>();
Element extensionsParent = xml.getChild(EXTENSIONS_XML_NAME);
if (extensionsParent == null) {
return knownExtensionNames;
}
Iterator<?> it = extensionsParent.getChildren(EXTENSION_ELEMENT_NAME).iterator();
while (it.hasNext()) {
Element child = (Element) it.next();
String encodedValue = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME_ENCODED);
if (encodedValue != null) {
byte[] bytes = NumericUtilities.convertStringToBytes(encodedValue);
String decoded = new String(bytes, StandardCharsets.UTF_8);
knownExtensionNames.add(decoded);
}
else {
String name = child.getAttributeValue(EXTENSION_ATTRIBUTE_NAME);
knownExtensionNames.add(name);
}
}
return knownExtensionNames;
}
/**
* Finds all {@link PluginDescription} objects that match a given set of plugin classes. This
* effectively tells the caller which of the given plugins have been loaded by the class loader.
* <p>
* Note that this method does not take path/package information into account when finding
* plugins; in the example above, if there is more than one plugin with the name "FooPlugin",
* only one will be found (the one found is not guaranteed to be the first).
*
* @param plugins the list of plugin classes to search for
* @return list of plugin descriptions
*/
private List<PluginDescription> getPluginDescriptions(Set<Class<?>> plugins) {
// First define the list of plugin descriptions to return
List<PluginDescription> descriptions = new ArrayList<>();
// Get all plugins that have been loaded
PluginsConfiguration pluginsConfiguration = tool.getPluginsConfiguration();
List<PluginDescription> allPluginDescriptions =
pluginsConfiguration.getManagedPluginDescriptions();
// see if an entry exists in the list of all loaded plugins
for (Class<?> plugin : plugins) {
String pluginName = plugin.getSimpleName();
Optional<PluginDescription> desc = allPluginDescriptions.stream()
.filter(d -> (pluginName.equals(d.getName())))
.findAny();
if (desc.isPresent()) {
descriptions.add(desc.get());
}
}
return descriptions;
}
private static Set<Class<?>> findLoadedPlugins(Set<ExtensionDetails> extensions) {
Set<PluginPath> pluginPaths = getPluginPaths();
Set<Class<?>> extensionPlugins = new HashSet<>();
for (ExtensionDetails extension : extensions) {
File installDir = extension.getInstallDir();
if (installDir == null) {
continue;
}
Set<Class<?>> classes = findPluginsLoadedFromExtension(installDir, pluginPaths);
extensionPlugins.addAll(classes);
}
return extensionPlugins;
}
private static Set<PluginPath> getPluginPaths() {
Set<PluginPath> paths = new HashSet<>();
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
for (Class<? extends Plugin> plugin : plugins) {
paths.add(new PluginPath(plugin));
}
return paths;
}
/**
* Finds all plugin classes loaded from a particular extension folder.
* <p>
* This uses the {@link ClassSearcher} to find all <code>Plugin.class</code> objects on the
* classpath. For each class, the original resource file is compared against the
* given extension folder and the jar files for that extension.
*
* @param dir the directory to search, or a jar file
* @param pluginPaths all loaded plugin paths
* @return list of {@link Plugin} classes, or empty list if none found
*/
private static Set<Class<?>> findPluginsLoadedFromExtension(File dir,
Set<PluginPath> pluginPaths) {
Set<Class<?>> result = new HashSet<>();
// Find any jar files in the directory provided
Set<String> jarPaths = getJarPaths(dir);
// Now get all Plugin.class file paths and see if any of them were loaded from one of the
// extension the given extension directory
for (PluginPath pluginPath : pluginPaths) {
if (pluginPath.isFrom(dir)) {
result.add(pluginPath.getPluginClass());
continue;
}
for (String jarPath : jarPaths) {
if (pluginPath.isFrom(jarPath)) {
result.add(pluginPath.getPluginClass());
}
}
}
return result;
}
private static Set<String> getJarPaths(File dir) {
Set<File> jarFiles = new HashSet<>();
findJarFiles(dir, jarFiles);
Set<String> paths = new HashSet<>();
for (File jar : jarFiles) {
try {
URL jarUrl = jar.toURI().toURL();
paths.add(jarUrl.getPath());
}
catch (MalformedURLException e) {
continue;
}
}
return paths;
}
/**
* Populates the given list with all discovered jar files found in the given directory and
* its subdirectories.
*
* @param dir the directory to search
* @param jarFiles list of found jar files
*/
private static void findJarFiles(File dir, Set<File> jarFiles) {
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
findJarFiles(f, jarFiles);
}
if (f.isFile() && f.getName().endsWith(".jar")) {
jarFiles.add(f);
}
}
}
private static class PluginPath {
private Class<? extends Plugin> pluginClass;
private String pluginLocation;
private File pluginFile;
PluginPath(Class<? extends Plugin> pluginClass) {
this.pluginClass = pluginClass;
String name = pluginClass.getName();
URL url = pluginClass.getResource('/' + name.replace('.', '/') + ".class");
this.pluginLocation = url.getPath();
this.pluginFile = new File(pluginLocation);
}
public boolean isFrom(File dir) {
return FileUtilities.isPathContainedWithin(dir, pluginFile);
}
boolean isFrom(String jarPath) {
return pluginLocation.contains(jarPath);
}
Class<? extends Plugin> getPluginClass() {
return pluginClass;
}
@Override
public String toString() {
return Json.toString(this);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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"));
}

View File

@ -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.

View File

@ -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);
}
}
}

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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!)

View File

@ -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;

View File

@ -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>

View File

@ -409,7 +409,7 @@ can be found in the <i>&lt;GhidraInstallDir&gt;</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 &#8594; Install Extensions...</b></li>
<li>Click <b>File &#8594; 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>