mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-27 04:05:28 +08:00
Updated to handle checking for pre-installed extensions
This commit is contained in:
+333
@@ -0,0 +1,333 @@
|
||||
/* ###
|
||||
* 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 static org.junit.Assert.*;
|
||||
|
||||
import java.awt.Window;
|
||||
import java.io.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.jar.*;
|
||||
|
||||
import javax.tools.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import docking.DialogComponentProvider;
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.GhidraClassLoader;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.ApplicationConfiguration;
|
||||
import ghidra.framework.project.extensions.ExtensionInstaller;
|
||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
import ghidra.test.TestEnv;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.extensions.ExtensionDetails;
|
||||
import ghidra.util.extensions.ExtensionUtils;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import util.CollectionUtils;
|
||||
import utilities.util.FileUtilities;
|
||||
import utility.application.ApplicationLayout;
|
||||
import utility.module.ModuleUtilities;
|
||||
|
||||
public class ExtensionManagerIntegrationTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
|
||||
private ApplicationLayout appLayout;
|
||||
|
||||
@Override
|
||||
protected ApplicationConfiguration createApplicationConfiguration() {
|
||||
|
||||
// This is a workaround to get the ClassSearcher to use the custom extensions class loader,
|
||||
// which is required, since the standard class loader's classpath was set by the time this
|
||||
// code gets run. Without this, the standard class loader cannot find our new extension.
|
||||
// The classpath is normally managed by the GhidraLauncher in a non-test environment.
|
||||
System.setProperty(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY,
|
||||
Boolean.TRUE.toString());
|
||||
|
||||
return super.createApplicationConfiguration();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
|
||||
appLayout = Application.getApplicationLayout();
|
||||
|
||||
ExtensionUtils.clearCache();
|
||||
deleteExtensionDirs();
|
||||
createExtensionDirs();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewExtensionPromptsUser() throws Exception {
|
||||
|
||||
//
|
||||
// Create a new extension with a plugin
|
||||
//
|
||||
File srcExtensionFolder = createExternalExtensionInFolder();
|
||||
assertTrue(ExtensionInstaller.install(srcExtensionFolder));
|
||||
|
||||
//
|
||||
// Update classpath the extension (this is normally done by the GhidraLauncher)
|
||||
//
|
||||
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
|
||||
assertEquals(1, extensions.size());
|
||||
ExtensionDetails extension = CollectionUtils.get(extensions);
|
||||
File extensionFolder = extension.getInstallDir();
|
||||
String jarFile = extensionFolder + File.separator + "lib/TestHelloWorldExtension.jar";
|
||||
|
||||
String cp = System.getProperty("java.class.path");
|
||||
System.setProperty("java.class.path", cp + File.pathSeparator + jarFile);
|
||||
|
||||
setInstanceField("hasSearched", ClassSearcher.class, false);
|
||||
ClassSearcher.search(TaskMonitor.DUMMY);
|
||||
|
||||
//
|
||||
// Launch the tool and verify the user is prompted to load the new plugins
|
||||
//
|
||||
String title = "New Plugins Found!";
|
||||
launchTool(title);
|
||||
|
||||
DialogComponentProvider dialog = waitForDialogComponent(title);
|
||||
pressButtonByText(dialog, "Yes", false);
|
||||
|
||||
DialogComponentProvider installerDialog = waitForDialogComponent(title);
|
||||
close(installerDialog);
|
||||
}
|
||||
|
||||
private void launchTool(String title) throws Exception {
|
||||
|
||||
/*
|
||||
Launching the tool with new extensions will show a modal dialog. Tool startup is slow
|
||||
enough that we can't use the normal wait mechanism, as it will timeout.
|
||||
*/
|
||||
TestEnv env = new TestEnv();
|
||||
runSwing(() -> {
|
||||
env.launchDefaultTool();
|
||||
}, false);
|
||||
|
||||
int total = 0;
|
||||
int max = 20_000;
|
||||
int sleepyTime = 250;
|
||||
while (total < max) {
|
||||
sleep(sleepyTime);
|
||||
total += sleepyTime;
|
||||
Window w = getWindow(title);
|
||||
if (w != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File createExternalExtensionInFolder() throws Exception {
|
||||
File externalFolder = createTempDirectory("TestExtensionParentDir");
|
||||
return doCreateExternalExtensionInFolder(externalFolder, "TextExtension");
|
||||
}
|
||||
|
||||
private File doCreateExternalExtensionInFolder(File externalFolder, String extensionName)
|
||||
throws Exception {
|
||||
|
||||
String version = Application.getApplicationVersion();
|
||||
|
||||
ResourceFile root = new ResourceFile(new ResourceFile(externalFolder), extensionName);
|
||||
assertTrue(FileUtilities.mkdirs(root.getFile(false)));
|
||||
|
||||
// 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", extensionName);
|
||||
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();
|
||||
|
||||
createJarWithPluginClass(root);
|
||||
|
||||
Msg.debug(this, "extension dir: " + root);
|
||||
|
||||
return root.getFile(false);
|
||||
}
|
||||
|
||||
private void createJarWithPluginClass(ResourceFile root) throws Exception {
|
||||
|
||||
ResourceFile lib = new ResourceFile(root, "lib");
|
||||
assertTrue(FileUtilities.mkdirs(lib.getFile(false)));
|
||||
|
||||
String jarName = "TestHelloWorldExtension.jar";
|
||||
|
||||
File jarFile = createExtensionLibJar(jarName);
|
||||
|
||||
Msg.debug(this, "wrote jar: " + jarFile);
|
||||
|
||||
File libFile = new File(lib.getFile(false), jarName);
|
||||
FileUtilities.copyFile(jarFile, libFile, false, TaskMonitor.DUMMY);
|
||||
}
|
||||
|
||||
private File createExtensionLibJar(String jarName) throws Exception {
|
||||
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||
|
||||
File tempFile = createTempFileForTest();
|
||||
File tempParent = tempFile.getParentFile();
|
||||
File jarFile = new File(tempParent, jarName);
|
||||
tempFile.renameTo(jarFile);
|
||||
|
||||
FileOutputStream fos = new FileOutputStream(jarFile);
|
||||
JarOutputStream jos = new JarOutputStream(fos, manifest);
|
||||
|
||||
File classDir = compileExtensionPlugin();
|
||||
Msg.debug(this, "class file: " + classDir);
|
||||
File[] files = classDir.listFiles((dir, name) -> name.endsWith(".class"));
|
||||
File classFile = files[0];
|
||||
|
||||
JarEntry dirEntry = new JarEntry(classDir.getName() + "/");
|
||||
jos.putNextEntry(dirEntry);
|
||||
jos.closeEntry();
|
||||
|
||||
JarEntry fileEntry = new JarEntry(dirEntry.getName() + classFile.getName());
|
||||
jos.putNextEntry(fileEntry);
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(classFile)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = fis.read(buffer)) != -1) {
|
||||
jos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
jos.closeEntry();
|
||||
|
||||
jos.close();
|
||||
fos.close();
|
||||
|
||||
Msg.debug(this, "jar file: " + jarFile);
|
||||
|
||||
return jarFile;
|
||||
}
|
||||
|
||||
private File compileExtensionPlugin() throws Exception {
|
||||
|
||||
// 1. Get the system Java compiler
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
if (compiler == null) {
|
||||
System.err.println("JDK not found. Make sure you are running a JDK, not a JRE.");
|
||||
fail();
|
||||
return null;
|
||||
}
|
||||
|
||||
File sourceFile = File.createTempFile("tmp", ".tmp");
|
||||
File dir = sourceFile.getParentFile();
|
||||
File packageDir = new File(dir, "testplugin");
|
||||
packageDir.mkdirs();
|
||||
File packagedSource = new File(packageDir, "TestHelloWorldPlugin.java");
|
||||
|
||||
FileUtilities.writeStringToFile(packagedSource,
|
||||
"""
|
||||
package testplugin;
|
||||
|
||||
import ghidra.app.ExamplesPluginPackage;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
|
||||
//@formatter:off
|
||||
@PluginInfo(
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = ExamplesPluginPackage.NAME,
|
||||
category = PluginCategoryNames.EXAMPLES,
|
||||
shortDescription = "Displays 'Hello World'",
|
||||
description = "Test plugin"
|
||||
)
|
||||
//@formatter:on
|
||||
public class TestHelloWorldPlugin extends Plugin {
|
||||
public TestHelloWorldPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
|
||||
|
||||
StandardJavaFileManager fileManager =
|
||||
compiler.getStandardFileManager(diagnostics, null, null);
|
||||
|
||||
Iterable<? extends JavaFileObject> compilationUnits =
|
||||
fileManager.getJavaFileObjects(packagedSource);
|
||||
|
||||
// 5. Create a compilation task
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(
|
||||
null, // output writer (uses System.err if null)
|
||||
fileManager, // file manager (uses standard if null)
|
||||
diagnostics, // diagnostic listener (uses System.err if null)
|
||||
null, // options (pass null for no options)
|
||||
null, // classes for annotation processing
|
||||
compilationUnits // source files
|
||||
);
|
||||
|
||||
// 6. Execute the compilation task
|
||||
boolean success = task.call();
|
||||
|
||||
// 7. Process the diagnostics
|
||||
if (success) {
|
||||
System.out.println("Compilation successful.");
|
||||
}
|
||||
else {
|
||||
System.out.println("Compilation failed.");
|
||||
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
|
||||
System.out.println(diagnostic.getKind() + ": " + diagnostic.getMessage(null) +
|
||||
" (Line: " + diagnostic.getLineNumber() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
fileManager.close();
|
||||
|
||||
return packageDir;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteExtensionDirs() {
|
||||
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
|
||||
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
|
||||
FileUtilities.deleteDir(installDir.getFile(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
/* ###
|
||||
* 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 static org.junit.Assert.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import org.jdom2.Element;
|
||||
import org.jdom2.output.Format;
|
||||
import org.jdom2.output.XMLOutputter;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.project.tool.testplugins.TestExtensionHello2Plugin;
|
||||
import ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin;
|
||||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
import ghidra.util.extensions.ExtensionUtils;
|
||||
import ghidra.util.xml.GenericXMLOutputter;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
import utilities.util.FileUtilities;
|
||||
import utility.application.ApplicationLayout;
|
||||
|
||||
public class ExtensionManagerTest extends AbstractGhidraHeadedIntegrationTest {
|
||||
|
||||
private ApplicationLayout appLayout;
|
||||
private FakeToolExtensionsEnabledState extensionsState;
|
||||
private ToolExtensionsStatusManager extensionManager;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
|
||||
appLayout = Application.getApplicationLayout();
|
||||
|
||||
ExtensionUtils.clearCache();
|
||||
deleteExtensionDirs();
|
||||
createExtensionDirs();
|
||||
|
||||
extensionsState = new FakeToolExtensionsEnabledState();
|
||||
extensionManager = new ToolExtensionsStatusManager(extensionsState);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNewExtensionPromptsUser() throws Exception {
|
||||
|
||||
// install new extension with a plugin
|
||||
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
|
||||
|
||||
// call extension manager using xml with no knowledge of the new extension
|
||||
Element element = XmlUtilities.fromString("""
|
||||
<ROOT>
|
||||
<EXTENSIONS>
|
||||
</EXTENSIONS>
|
||||
</ROOT>
|
||||
""");
|
||||
extensionManager.restoreFromXml(element);
|
||||
|
||||
// verify user is prompted to add new plugins
|
||||
extensionManager.checkForNewExtensions();
|
||||
assertTrue(extensionsState.didPrompt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKnownExtensionDoesNotPromptUser() throws Exception {
|
||||
|
||||
// install extension with a plugin
|
||||
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
|
||||
|
||||
// call extension manager with xml describing the extension
|
||||
//@formatter:off
|
||||
Element element = XmlUtilities.fromString(
|
||||
"""
|
||||
<ROOT>
|
||||
<EXTENSIONS>
|
||||
<EXTENSION NAME="FooExtension">
|
||||
<PLUGIN CLASS="ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin" />
|
||||
</EXTENSION>
|
||||
</EXTENSIONS>
|
||||
</ROOT>
|
||||
""");
|
||||
//@formatter:on
|
||||
extensionManager.restoreFromXml(element);
|
||||
|
||||
// verify no prompt
|
||||
extensionManager.checkForNewExtensions();
|
||||
assertFalse(extensionsState.didPrompt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKnownExetension_OldStyleXml_UninstalledPlugins_PromptsUser() throws Exception {
|
||||
|
||||
//
|
||||
// Tests that old xml describing a known extension triggers a prompt. The old style xml
|
||||
// does not contain plugin names. Thus, we have no way of knowing if there are new plugins,
|
||||
// so we prompt. Tests support for tools with the old xml migrating to the new xml.
|
||||
//
|
||||
|
||||
// install extension with a plugin
|
||||
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
|
||||
|
||||
// Call extension manager with old style xml describing the extension. This knows about the
|
||||
// extension, but not the plugins inside.
|
||||
Element element = XmlUtilities.fromString("""
|
||||
<ROOT>
|
||||
<EXTENSIONS>
|
||||
<EXTENSION NAME="FooExtension" />
|
||||
</EXTENSIONS>
|
||||
</ROOT>
|
||||
""");
|
||||
extensionManager.restoreFromXml(element);
|
||||
|
||||
// verify user is prompted to add new plugins
|
||||
extensionManager.checkForNewExtensions();
|
||||
assertTrue(extensionsState.didPrompt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKnownExetension_OldStyleXml_AllPluginsInstalled_DoesNotPromptUser()
|
||||
throws Exception {
|
||||
|
||||
//
|
||||
// Given old style xml with knowledge of an extension, all extension plugins are installed,
|
||||
// the user is not prompted to install plugins. Tests support for tools with the old xml
|
||||
// migrating to the new xml.
|
||||
//
|
||||
|
||||
// install extension with a plugin
|
||||
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
|
||||
|
||||
// mark plugin as installed
|
||||
extensionsState.setInstalled(TestExtensionHelloPlugin.class);
|
||||
|
||||
// Call extension manager with old style xml describing the extension. This knows about the
|
||||
// extension, but not the plugins inside.
|
||||
Element element = XmlUtilities.fromString("""
|
||||
<ROOT>
|
||||
<EXTENSIONS>
|
||||
<EXTENSION NAME="FooExtension" />
|
||||
</EXTENSIONS>
|
||||
</ROOT>
|
||||
""");
|
||||
extensionManager.restoreFromXml(element);
|
||||
|
||||
// verify no prompt, since the plugins in the extension have already been installed
|
||||
extensionManager.checkForNewExtensions();
|
||||
assertFalse(extensionsState.didPrompt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKnownExtension_UpdatedWithNewPlugins_PromptsUser() throws Exception {
|
||||
|
||||
//
|
||||
// Given an extension known in the xml, with a list of known plugins, and an updated version
|
||||
// of that extension is installed, the user is prompted to add plugins.
|
||||
//
|
||||
|
||||
// install an extension with one known plugin and one new plugin
|
||||
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class,
|
||||
TestExtensionHello2Plugin.class);
|
||||
|
||||
// call extension manager with new style xml that only knows about the original plugin
|
||||
//@formatter:off
|
||||
Element element = XmlUtilities.fromString(
|
||||
"""
|
||||
<ROOT>
|
||||
<EXTENSIONS>
|
||||
<EXTENSION NAME="FooExtension">
|
||||
<PLUGIN CLASS="ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin.class" />
|
||||
</EXTENSION>
|
||||
</EXTENSIONS>
|
||||
</ROOT>
|
||||
""");
|
||||
//@formatter:on
|
||||
extensionManager.restoreFromXml(element);
|
||||
|
||||
// verify user is prompted to add new plugins
|
||||
extensionManager.checkForNewExtensions();
|
||||
assertTrue(extensionsState.didPrompt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveToXml_NoExtensions() throws Exception {
|
||||
|
||||
Element rootElement = new Element("ROOT");
|
||||
extensionManager.saveToXml(rootElement);
|
||||
|
||||
String expectedXml =
|
||||
"""
|
||||
<ROOT>
|
||||
<EXTENSIONS />
|
||||
</ROOT>
|
||||
""".trim();
|
||||
String actualXml = toString(rootElement);
|
||||
|
||||
assertEquals(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
private String toString(Element e) {
|
||||
XMLOutputter outputter = GenericXMLOutputter.getInstance();
|
||||
Format format = outputter.getFormat();
|
||||
format.setLineSeparator(System.lineSeparator());
|
||||
return outputter.outputString(e);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSaveToXml_OneExtension() throws Exception {
|
||||
|
||||
extensionsState.addExtension("FooExtension", TestExtensionHelloPlugin.class);
|
||||
|
||||
Element rootElement = new Element("ROOT");
|
||||
extensionManager.saveToXml(rootElement);
|
||||
|
||||
//@formatter:off
|
||||
String expectedXml =
|
||||
"""
|
||||
<ROOT>
|
||||
<EXTENSIONS>
|
||||
<EXTENSION NAME="FooExtension">
|
||||
<PLUGIN CLASS="ghidra.framework.project.tool.testplugins.TestExtensionHelloPlugin" />
|
||||
</EXTENSION>
|
||||
</EXTENSIONS>
|
||||
</ROOT>
|
||||
""".trim();
|
||||
//@formatter:on
|
||||
String actualXml = toString(rootElement);
|
||||
|
||||
assertEquals(expectedXml, actualXml);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteExtensionDirs() {
|
||||
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
|
||||
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
|
||||
FileUtilities.deleteDir(installDir.getFile(false));
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeToolExtensionsEnabledState implements ExtensionsEnabledState {
|
||||
|
||||
private Map<String, Set<Class<?>>> extensions = new HashMap<>();
|
||||
private Set<Class<?>> installedPlugins = new HashSet<>();
|
||||
private boolean didPrompt = false;
|
||||
|
||||
@Override
|
||||
public Map<String, Set<Class<?>>> getAllKnownExtensions() {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeInstalledPlugins(Set<Class<?>> plugins) {
|
||||
plugins.removeAll(installedPlugins);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
|
||||
didPrompt = true;
|
||||
}
|
||||
|
||||
void addExtension(String name, Class<?>... classes) {
|
||||
extensions.put(name, Set.of(classes));
|
||||
}
|
||||
|
||||
boolean didPrompt() {
|
||||
return didPrompt;
|
||||
}
|
||||
|
||||
void setInstalled(Class<TestExtensionHelloPlugin> c) {
|
||||
installedPlugins.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/* ###
|
||||
* 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.testplugins;
|
||||
|
||||
import ghidra.app.ExamplesPluginPackage;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
//@formatter:off
|
||||
@PluginInfo(
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = ExamplesPluginPackage.NAME,
|
||||
category = PluginCategoryNames.EXAMPLES,
|
||||
shortDescription = "Displays 'Hello World'",
|
||||
description = "Test plugin"
|
||||
)
|
||||
//@formatter:on
|
||||
public class TestExtensionHello2Plugin extends Plugin {
|
||||
|
||||
public TestExtensionHello2Plugin(PluginTool tool) {
|
||||
super(tool);
|
||||
|
||||
Msg.info(this, "Test Hello World constructed!");
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/* ###
|
||||
* 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.testplugins;
|
||||
|
||||
import ghidra.app.ExamplesPluginPackage;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
//@formatter:off
|
||||
@PluginInfo(
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = ExamplesPluginPackage.NAME,
|
||||
category = PluginCategoryNames.EXAMPLES,
|
||||
shortDescription = "Displays 'Hello World'",
|
||||
description = "Test plugin"
|
||||
)
|
||||
//@formatter:on
|
||||
public class TestExtensionHelloPlugin extends Plugin {
|
||||
|
||||
/*
|
||||
A plugin for testing extension loading
|
||||
*/
|
||||
|
||||
public TestExtensionHelloPlugin(PluginTool tool) {
|
||||
super(tool);
|
||||
|
||||
Msg.info(this, "Test Hello World constructed!");
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -268,8 +268,13 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@return true if this extension is installed under an installation folder or inside of a
|
||||
* source control repository folder}
|
||||
* Returns true if this extension is installed under an installation folder or inside of a
|
||||
* source control repository folder.
|
||||
* <p>
|
||||
* In a repo, extension modules live under repo/Ghidra/Extensions. In an installation, there
|
||||
* may exist pre-installed extensions under installDir/Ghidra/Extensions.
|
||||
*
|
||||
* @return true if this extension is installed under an installation folder
|
||||
*/
|
||||
public boolean isInstalledInInstallationFolder() {
|
||||
if (installDir == null) {
|
||||
@@ -284,7 +289,10 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
|
||||
return false;
|
||||
}
|
||||
|
||||
// extDirs.get(0) is the user extension dir
|
||||
// extDirs.get(0) is the user extension dir (e.g., '.config/ghidra/ghidra<version>/Extensions
|
||||
// The remaining dirs are of the form:
|
||||
// <repo>/Ghidra/Extensions
|
||||
// <install dir>/Ghidra/Extensions
|
||||
return extDirs.stream()
|
||||
.skip(1)
|
||||
.anyMatch(
|
||||
|
||||
@@ -242,8 +242,8 @@ public class ExtensionUtils {
|
||||
}
|
||||
|
||||
Set<ExtensionDetails> results = new HashSet<>();
|
||||
findExtensionsInZips(archiveFiles, results);
|
||||
findExtensionsInFolder(archiveDir.getFile(false), results);
|
||||
findExtensionsInArchiveZips(archiveFiles, results);
|
||||
findExtensionsInArchiveSubfolder(archiveDir.getFile(false), results);
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -258,7 +258,7 @@ public class ExtensionUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
|
||||
private static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
|
||||
|
||||
File file = resourceFile.getFile(false);
|
||||
if (!isZip(file)) {
|
||||
@@ -279,7 +279,7 @@ public class ExtensionUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void findExtensionsInZips(ResourceFile[] archiveFiles,
|
||||
private static void findExtensionsInArchiveZips(ResourceFile[] archiveFiles,
|
||||
Set<ExtensionDetails> results) {
|
||||
for (ResourceFile file : archiveFiles) {
|
||||
ExtensionDetails extension = ExtensionUtils.createExtensionDetailsFromArchive(file);
|
||||
@@ -318,7 +318,7 @@ public class ExtensionUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void findExtensionsInFolder(File dir, Set<ExtensionDetails> results) {
|
||||
private static void findExtensionsInArchiveSubfolder(File dir, Set<ExtensionDetails> results) {
|
||||
List<File> propFiles = findExtensionPropertyFiles(dir);
|
||||
for (File propFile : propFiles) {
|
||||
ExtensionDetails extension = ExtensionUtils.createExtensionFromProperties(propFile);
|
||||
@@ -326,8 +326,8 @@ public class ExtensionUtils {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We found this extension in the installation directory, so set the archive path
|
||||
// property and add to the final set.
|
||||
// We found this extension in the installation archive directory, so set the archive
|
||||
// path property and add to the final set.
|
||||
File extDir = propFile.getParentFile();
|
||||
extension.setArchivePath(extDir.getAbsolutePath());
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@ public class XmlUtilities {
|
||||
int codePoint = xml.codePointAt(offset);
|
||||
offset += Character.charCount(codePoint);
|
||||
|
||||
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) && (codePoint != 0x0D)) {
|
||||
if ((codePoint < ' ') && (codePoint != 0x09) && (codePoint != 0x0A) &&
|
||||
(codePoint != 0x0D)) {
|
||||
continue;
|
||||
}
|
||||
if (codePoint >= 0x7F) {
|
||||
@@ -189,10 +190,10 @@ public class XmlUtilities {
|
||||
/**
|
||||
* Convert a String into a JDOM {@link Element}.
|
||||
*
|
||||
* @param s
|
||||
* @return
|
||||
* @throws JDOMException
|
||||
* @throws IOException
|
||||
* @param s the xml string
|
||||
* @return an element
|
||||
* @throws JDOMException if there is an exception building the element
|
||||
* @throws IOException if there is an exception building the element
|
||||
*/
|
||||
public static Element fromString(String s) throws JDOMException, IOException {
|
||||
SAXBuilder sax = createSecureSAXBuilder(false, false);
|
||||
@@ -547,7 +548,7 @@ public class XmlUtilities {
|
||||
|
||||
/**
|
||||
* Parses the given string into a boolean value. Acceptable inputs are
|
||||
* y,n,true,fase. A null input string will return false (useful if optional
|
||||
* y,n,true,false. A null input string will return false (useful if optional
|
||||
* boolean attribute is false by default)
|
||||
*
|
||||
* @param boolStr the string to parse into a boolean value
|
||||
|
||||
+4
-4
@@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@@ -78,7 +78,7 @@ public class ExtensionInstaller {
|
||||
}
|
||||
|
||||
Extensions extensions = ExtensionUtils.getAllInstalledExtensions();
|
||||
if (checkForConflictWithDevelopmentExtension(extension, extensions)) {
|
||||
if (checkForConflictWithPreInstalledExtension(extension, extensions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ public class ExtensionInstaller {
|
||||
buffy.toString());
|
||||
}
|
||||
|
||||
private static boolean checkForConflictWithDevelopmentExtension(ExtensionDetails newExtension,
|
||||
private static boolean checkForConflictWithPreInstalledExtension(ExtensionDetails newExtension,
|
||||
Extensions extensions) {
|
||||
|
||||
String name = newExtension.getName();
|
||||
|
||||
+2
-1
@@ -94,7 +94,8 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not allow GUI removal of extensions manually installed in installation directory.
|
||||
// Do not allow GUI removal of extensions manually installed in installation directory or
|
||||
// in a repo directory.
|
||||
ExtensionDetails extension = getSelectedExtension(rowIndex);
|
||||
if (extension.isInstalledInInstallationFolder()) {
|
||||
return false;
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/* ###
|
||||
* 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.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An interface to help describe extensions' enable state for a given tool.
|
||||
*/
|
||||
public interface ExtensionsEnabledState {
|
||||
|
||||
/**
|
||||
* {@return a map of all known extensions to a set of their plugins}
|
||||
*/
|
||||
public Map<String, Set<Class<?>>> getAllKnownExtensions();
|
||||
|
||||
/**
|
||||
* All plugins installed in the current tool will be removed from the given set. This allows the
|
||||
* client to have a set of plugins that are not currently installed.
|
||||
* @param allPlugins the plugins set to update
|
||||
*/
|
||||
public void removeInstalledPlugins(Set<Class<?>> allPlugins);
|
||||
|
||||
/**
|
||||
* Shows a window to prompt the user to configure any new extension plugins.
|
||||
* @param newPlugins the new extension plugins
|
||||
*/
|
||||
public void propmtToConfigureNewPlugins(Set<Class<?>> newPlugins);
|
||||
}
|
||||
+4
-3
@@ -44,7 +44,7 @@ public class GhidraTool extends PluginTool {
|
||||
private FileOpenDropHandler fileOpenDropHandler;
|
||||
private DockingAction configureToolAction;
|
||||
|
||||
private ExtensionManager extensionManager;
|
||||
private ToolExtensionsStatusManager extensionManager;
|
||||
private boolean hasBeenShown;
|
||||
|
||||
/**
|
||||
@@ -72,9 +72,10 @@ public class GhidraTool extends PluginTool {
|
||||
* extension manager.
|
||||
* @return the extension manager
|
||||
*/
|
||||
private ExtensionManager getExtensionManager() {
|
||||
private ToolExtensionsStatusManager getExtensionManager() {
|
||||
if (extensionManager == null) {
|
||||
extensionManager = new ExtensionManager(this);
|
||||
ToolExtensionsEnabledState state = new ToolExtensionsEnabledState(this);
|
||||
extensionManager = new ToolExtensionsStatusManager(state);
|
||||
}
|
||||
return extensionManager;
|
||||
}
|
||||
|
||||
+91
-144
@@ -17,52 +17,59 @@ package ghidra.framework.project.tool;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jdom2.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.util.NumericUtilities;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.extensions.ExtensionDetails;
|
||||
import ghidra.util.extensions.ExtensionUtils;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
* A class to manage saving and restoring of known extension used by this tool.
|
||||
* The default extension state for a {@link PluginTool}.
|
||||
*/
|
||||
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";
|
||||
class ToolExtensionsEnabledState implements ExtensionsEnabledState {
|
||||
|
||||
private PluginTool tool;
|
||||
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
|
||||
|
||||
ExtensionManager(PluginTool tool) {
|
||||
ToolExtensionsEnabledState(PluginTool tool) {
|
||||
this.tool = tool;
|
||||
}
|
||||
|
||||
void checkForNewExtensions() {
|
||||
if (newExtensionPlugins.isEmpty()) {
|
||||
return;
|
||||
@Override
|
||||
public Map<String, Set<Class<?>>> getAllKnownExtensions() {
|
||||
|
||||
Set<ExtensionDetails> extensions = getExtensions();
|
||||
if (extensions.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
propmtToConfigureNewPlugins(newExtensionPlugins);
|
||||
newExtensionPlugins.clear();
|
||||
Map<String, Set<Class<?>>> plugins = new HashMap<>();
|
||||
Set<PluginPath> pluginPaths = getAllPluginPaths();
|
||||
for (ExtensionDetails extension : extensions) {
|
||||
Set<Class<?>> classes = findPluginsLoadedFromExtension(extension, pluginPaths);
|
||||
plugins.put(extension.getName(), classes);
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
private void propmtToConfigureNewPlugins(Set<Class<?>> plugins) {
|
||||
@Override
|
||||
public void removeInstalledPlugins(Set<Class<?>> plugins) {
|
||||
List<Plugin> activePlugins = tool.getManagedPlugins();
|
||||
for (Plugin plugin : activePlugins) {
|
||||
Class<? extends Plugin> clazz = plugin.getClass();
|
||||
plugins.remove(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public 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?");
|
||||
@@ -74,130 +81,7 @@ class ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
Set<Class<?>> classes = findPluginsLoadedFromExtension(extension, pluginPaths);
|
||||
extensionPlugins.addAll(classes);
|
||||
}
|
||||
|
||||
return extensionPlugins;
|
||||
}
|
||||
|
||||
private static Set<PluginPath> getPluginPaths() {
|
||||
private static Set<PluginPath> getAllPluginPaths() {
|
||||
Set<PluginPath> paths = new HashSet<>();
|
||||
List<Class<? extends Plugin>> plugins = ClassSearcher.getClasses(Plugin.class);
|
||||
for (Class<? extends Plugin> plugin : plugins) {
|
||||
@@ -206,6 +90,33 @@ class ExtensionManager {
|
||||
return paths;
|
||||
}
|
||||
|
||||
private static Set<ExtensionDetails> getExtensions() {
|
||||
Set<ExtensionDetails> installedExtensions = ExtensionUtils.getActiveInstalledExtensions();
|
||||
return installedExtensions.stream()
|
||||
.filter(e -> !isRepoExtension(e))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* We wish to ignore extension modules that live in the repo installation dir. This keeps
|
||||
* developers from getting prompted while developing.
|
||||
* @param e the extension
|
||||
* @return true if not a development extension
|
||||
*/
|
||||
private static boolean isRepoExtension(ExtensionDetails e) {
|
||||
// Repo extensions live in a known installation folder in development mode. They do not
|
||||
// exist in a release.
|
||||
if (SystemUtilities.isInDevelopmentMode()) {
|
||||
if (e.isInstalledInInstallationFolder()) {
|
||||
// Checking for a build file is an easy way to find repo extensions
|
||||
File dir = e.getInstallDir();
|
||||
File buildFile = new File(dir, "build.gradle");
|
||||
return buildFile.exists();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all plugin classes loaded from a particular extension folder.
|
||||
* <p>
|
||||
@@ -245,6 +156,42 @@ class ExtensionManager {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 class PluginPath {
|
||||
private Class<? extends Plugin> pluginClass;
|
||||
private String pluginLocation;
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
/* ###
|
||||
* 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.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jdom2.Element;
|
||||
|
||||
import ghidra.util.NumericUtilities;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
|
||||
/**
|
||||
* A class to manage saving and restoring of known extension used by a tool.
|
||||
*/
|
||||
class ToolExtensionsStatusManager {
|
||||
|
||||
private static final String XML_TAG_EXTENSIONS = "EXTENSIONS";
|
||||
private static final String XML_TAG_EXTENSION = "EXTENSION";
|
||||
private static final String XML_TAG_PLUGIN = "PLUGIN";
|
||||
private static final String XML_ATTR_EXTENSION_NAME_ENCODED = "ENCODED_NAME";
|
||||
private static final String XML_ATTR_EXTENSION_NAME = "NAME";
|
||||
private static final String XML_ATTR_EXTENSION_PLUGIN_CLASS = "CLASS";
|
||||
|
||||
private Set<Class<?>> newExtensionPlugins = new HashSet<>();
|
||||
private ExtensionsEnabledState extensionsState;
|
||||
|
||||
ToolExtensionsStatusManager(ExtensionsEnabledState extensionState) {
|
||||
this.extensionsState = extensionState;
|
||||
}
|
||||
|
||||
void checkForNewExtensions() {
|
||||
if (newExtensionPlugins.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run later to not block the opening of the tool.
|
||||
Swing.runLater(() -> {
|
||||
extensionsState.propmtToConfigureNewPlugins(newExtensionPlugins);
|
||||
newExtensionPlugins.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void saveToXml(Element xml) {
|
||||
|
||||
Map<String, Set<Class<?>>> pluginsByExtension =
|
||||
extensionsState.getAllKnownExtensions();
|
||||
Element extensionsParent = new Element(XML_TAG_EXTENSIONS);
|
||||
|
||||
Set<Entry<String, Set<Class<?>>>> entries = pluginsByExtension.entrySet();
|
||||
for (Entry<String, Set<Class<?>>> entry : entries) {
|
||||
String name = entry.getKey();
|
||||
|
||||
Element extensionsElement = new Element(XML_TAG_EXTENSION);
|
||||
setExtensionName(extensionsElement, name);
|
||||
extensionsParent.addContent(extensionsElement);
|
||||
|
||||
Set<Class<?>> plugins = entry.getValue();
|
||||
for (Class<?> clazz : plugins) {
|
||||
String className = clazz.getName();
|
||||
Element pluginElement = new Element(XML_TAG_PLUGIN);
|
||||
pluginElement.setAttribute(XML_ATTR_EXTENSION_PLUGIN_CLASS, className);
|
||||
extensionsElement.addContent(pluginElement);
|
||||
}
|
||||
}
|
||||
|
||||
xml.addContent(extensionsParent);
|
||||
}
|
||||
|
||||
void restoreFromXml(Element xml) {
|
||||
|
||||
/*
|
||||
1) Grab all extension plugins currently found on the classpath. This will include all
|
||||
old and new extensions.
|
||||
|
||||
2) Grab all previously known extensions and plugins.
|
||||
|
||||
3) Find all entirely new extensions or extensions that have new plugins added.
|
||||
|
||||
4) Filter plugins by those already installed.
|
||||
|
||||
5) Save the new extension plugins for later user prompting when saving to xml.
|
||||
*/
|
||||
Map<String, Set<Class<?>>> extensionPlugins = extensionsState.getAllKnownExtensions();
|
||||
Map<String, ExtensionMemento> xmlMementosByName = getKnownExtensions(xml);
|
||||
Set<String> names = extensionPlugins.keySet();
|
||||
Set<String> newExtensions = new HashSet<>(names);
|
||||
for (String name : names) {
|
||||
ExtensionMemento xmlMemento = xmlMementosByName.get(name);
|
||||
if (xmlMemento == null) {
|
||||
continue; // new extension
|
||||
}
|
||||
|
||||
// The extension is known. If it doesn't have new plugins, then we can remove it from
|
||||
// the new extensions, assuming it has not changed.
|
||||
if (!hasNewPlugins(xmlMemento, extensionPlugins)) {
|
||||
newExtensions.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
Set<Class<?>> newPlugins = newExtensions.stream()
|
||||
.map(name -> extensionPlugins.get(name)) // classes by extension name
|
||||
.flatMap(set -> set.stream()) // map all sets to a single stream
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
extensionsState.removeInstalledPlugins(newPlugins);
|
||||
|
||||
// Get all plugins contained in the 'new' 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.
|
||||
newExtensionPlugins.addAll(newPlugins);
|
||||
}
|
||||
|
||||
private static boolean hasNewPlugins(ExtensionMemento xmlMemento,
|
||||
Map<String, Set<Class<?>>> pluginsByExtensionName) {
|
||||
|
||||
// If the xml memento is empty, it is either the old style xml that did not save plugin
|
||||
// names or is the new style xml, but the extension did not previously have any plugins. In
|
||||
// this case, we want to prompt the user if there are plugins to install.
|
||||
Set<String> xmlClassNames = xmlMemento.pluginClassNames();
|
||||
Set<Class<?>> cpPluginClasses = pluginsByExtensionName.get(xmlMemento.name());
|
||||
if (xmlClassNames.isEmpty() && !cpPluginClasses.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> cpNames = cpPluginClasses.stream()
|
||||
.map(c -> c.getName())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
cpNames.removeAll(xmlClassNames);
|
||||
return !cpNames.isEmpty();
|
||||
}
|
||||
|
||||
private static Map<String, ExtensionMemento> getKnownExtensions(Element xml) {
|
||||
|
||||
Set<ExtensionMemento> mementos = new HashSet<>();
|
||||
Element extensionsParent = xml.getChild(XML_TAG_EXTENSIONS);
|
||||
if (extensionsParent == null) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
for (Element child : extensionsParent.getChildren(XML_TAG_EXTENSION)) {
|
||||
List<Element> plugins = child.getChildren(XML_TAG_PLUGIN);
|
||||
Set<String> pluginClasses = getExtensionPluginClasses(plugins);
|
||||
String extensionName = readExtensionName(child);
|
||||
mementos.add(new ExtensionMemento(extensionName, pluginClasses));
|
||||
}
|
||||
|
||||
return mementos.stream()
|
||||
.collect(Collectors.toMap(
|
||||
ExtensionMemento::name,
|
||||
Function.identity()));
|
||||
}
|
||||
|
||||
private static Set<String> getExtensionPluginClasses(List<Element> plugins) {
|
||||
Set<String> pluginNames = new HashSet<>();
|
||||
for (Element element : plugins) {
|
||||
String className = element.getAttributeValue(XML_ATTR_EXTENSION_PLUGIN_CLASS);
|
||||
pluginNames.add(className);
|
||||
}
|
||||
return pluginNames;
|
||||
}
|
||||
|
||||
private static String readExtensionName(Element element) {
|
||||
String encodedValue = element.getAttributeValue(XML_ATTR_EXTENSION_NAME_ENCODED);
|
||||
if (encodedValue != null) {
|
||||
byte[] bytes = NumericUtilities.convertStringToBytes(encodedValue);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
return element.getAttributeValue(XML_ATTR_EXTENSION_NAME);
|
||||
}
|
||||
|
||||
private static void setExtensionName(Element element, String name) {
|
||||
if (XmlUtilities.hasInvalidXMLCharacters(name)) {
|
||||
element.setAttribute(XML_ATTR_EXTENSION_NAME_ENCODED, NumericUtilities
|
||||
.convertBytesToString(name.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
else {
|
||||
element.setAttribute(XML_ATTR_EXTENSION_NAME, name);
|
||||
}
|
||||
}
|
||||
|
||||
private static record ExtensionMemento(String name, Set<String> pluginClassNames) {
|
||||
|
||||
}
|
||||
}
|
||||
+19
-20
@@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@@ -48,10 +48,6 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -60,16 +56,10 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
|
||||
|
||||
setErrorGUIEnabled(false);
|
||||
|
||||
// clear static caching of extensions
|
||||
ExtensionUtils.clearCache();
|
||||
|
||||
appLayout = Application.getApplicationLayout();
|
||||
|
||||
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
|
||||
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
|
||||
FileUtilities.deleteDir(installDir.getFile(false));
|
||||
}
|
||||
|
||||
ExtensionUtils.clearCache();
|
||||
deleteExtensionDirs();
|
||||
createExtensionDirs();
|
||||
}
|
||||
|
||||
@@ -239,7 +229,8 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanupUninstalledExtions_WithExtensionMarkedForUninstall() throws Exception {
|
||||
public void testCleanupUninstalledExtensions_WithExtensionMarkedForUninstall()
|
||||
throws Exception {
|
||||
|
||||
File externalFolder = createExternalExtensionInFolder();
|
||||
assertTrue(ExtensionInstaller.install(externalFolder));
|
||||
@@ -256,7 +247,8 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanupUninstalledExtions_SomeExtensionMarkedForUninstall() throws Exception {
|
||||
public void testCleanupUninstalledExtensions_SomeExtensionMarkedForUninstall()
|
||||
throws Exception {
|
||||
|
||||
List<File> extensionFolders = createTwoExternalExtensionsInFolder();
|
||||
assertTrue(ExtensionInstaller.install(extensionFolders.get(0)));
|
||||
@@ -280,7 +272,7 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCleanupUninstalledExtions_NoExtensionsMarkedForUninstall() throws Exception {
|
||||
public void testCleanupUninstalledExtensions_NoExtensionsMarkedForUninstall() throws Exception {
|
||||
|
||||
File externalFolder = createExternalExtensionInFolder();
|
||||
assertTrue(ExtensionInstaller.install(externalFolder));
|
||||
@@ -426,14 +418,14 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
|
||||
|
||||
/*
|
||||
Create a zip file that looks something like this:
|
||||
|
||||
|
||||
/
|
||||
{Extension Name 1}/
|
||||
extension.properties
|
||||
|
||||
|
||||
{Extension Name 2}/
|
||||
extension.properties
|
||||
|
||||
|
||||
*/
|
||||
|
||||
errorsExpected(() -> {
|
||||
@@ -521,6 +513,13 @@ public class ExtensionInstallerTest extends AbstractDockingTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteExtensionDirs() {
|
||||
FileUtilities.deleteDir(appLayout.getExtensionArchiveDir().getFile(false));
|
||||
for (ResourceFile installDir : appLayout.getExtensionInstallationDirs()) {
|
||||
FileUtilities.deleteDir(installDir.getFile(false));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies that the installation folder is empty.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user