GP-3623 - Extensions - Added an extension-specific class loader; moved ExtensionUtils to Generic

This commit is contained in:
dragonmacher 2023-11-21 11:18:28 -05:00
parent 80d92aa32f
commit 0a520b08bd
30 changed files with 1079 additions and 731 deletions

View file

@ -31,11 +31,11 @@ import ghidra.framework.data.DomainObjectAdapter;
import ghidra.framework.main.FrontEndTool;
import ghidra.framework.model.*;
import ghidra.framework.project.DefaultProjectManager;
import ghidra.framework.project.extensions.ExtensionUtils;
import ghidra.framework.store.LockException;
import ghidra.program.database.ProgramDB;
import ghidra.util.*;
import ghidra.util.exception.UsrException;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.task.TaskLauncher;
/**

View file

@ -257,9 +257,9 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
// By default, create test output within a directory at the same level as the
// development repositories
outputRoot = Application.getApplicationRootDirectory()
.getParentFile()
.getParentFile()
.getCanonicalPath();
.getParentFile()
.getParentFile()
.getCanonicalPath();
}
catch (IOException e) {
throw new RuntimeException(e);
@ -938,7 +938,7 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
applicationRootDirectories = Application.getApplicationRootDirectories();
ResourceFile myModuleRootDirectory =
Application.getModuleContainingClass(getClass().getName());
Application.getModuleContainingClass(getClass());
if (myModuleRootDirectory != null) {
File myModuleRoot = myModuleRootDirectory.getFile(false);
if (myModuleRoot != null) {
@ -1672,7 +1672,7 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
RegisterValue thumbMode = new RegisterValue(tReg, BigInteger.ONE);
try {
program.getProgramContext()
.setRegisterValue(functionAddr, functionAddr, thumbMode);
.setRegisterValue(functionAddr, functionAddr, thumbMode);
}
catch (ContextChangeException e) {
throw new AssertException(e);
@ -1686,7 +1686,7 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
RegisterValue thumbMode = new RegisterValue(isaModeReg, BigInteger.ONE);
try {
program.getProgramContext()
.setRegisterValue(functionAddr, functionAddr, thumbMode);
.setRegisterValue(functionAddr, functionAddr, thumbMode);
}
catch (ContextChangeException e) {
throw new AssertException(e);
@ -1911,7 +1911,7 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
if (absoluteGzfFilePath.exists()) {
program = getGzfProgram(outputDir, gzfCachePath);
if (program != null && !MD5Utilities.getMD5Hash(testFile.file)
.equals(program.getExecutableMD5())) {
.equals(program.getExecutableMD5())) {
// remove obsolete GZF cache file
env.release(program);
program = null;
@ -1936,7 +1936,7 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
}
else {
program = env.getGhidraProject()
.importProgram(testFile.file, language, compilerSpec);
.importProgram(testFile.file, language, compilerSpec);
}
program.addConsumer(this);
env.getGhidraProject().close(program);
@ -1957,8 +1957,8 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
if (!program.getLanguageID().equals(language.getLanguageID()) ||
!program.getCompilerSpec()
.getCompilerSpecID()
.equals(compilerSpec.getCompilerSpecID())) {
.getCompilerSpecID()
.equals(compilerSpec.getCompilerSpecID())) {
throw new IOException((usingCachedGZF ? "Cached " : "") +
"Program has incorrect language/compiler spec (" + program.getLanguageID() +
"/" + program.getCompilerSpec().getCompilerSpecID() + "): " +
@ -2123,7 +2123,7 @@ public abstract class ProcessorEmulatorTestAdapter extends TestCase implements E
testFileDigest.append(nameAndAddr);
testFileDigest.append(" (GroupInfo @ ");
testFileDigest
.append(testGroup.controlBlock.getInfoStructureAddress().toString(true));
.append(testGroup.controlBlock.getInfoStructureAddress().toString(true));
testFileDigest.append(")");
if (duplicateTests.contains(testGroup.testGroupName)) {
testFileDigest.append(" *DUPLICATE*");

View file

@ -26,11 +26,11 @@ import generic.jar.*;
import ghidra.GhidraApplicationLayout;
import ghidra.GhidraLaunchable;
import ghidra.framework.*;
import ghidra.framework.project.extensions.ExtensionUtils;
import ghidra.util.classfinder.ClassFinder;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
@ -717,9 +717,6 @@ public class GhidraJarBuilder implements GhidraLaunchable {
jarOut.close();
}
/**
* Outputs an individual file to the jar.
*/
public void addFile(String jarPath, File file, ApplicationModule module)
throws IOException, CancelledException {
if (!file.exists()) {
@ -834,7 +831,7 @@ public class GhidraJarBuilder implements GhidraLaunchable {
zipOut.close();
}
/**
/*
* Outputs an individual file to the jar.
*/
public void addFile(String zipPath, File file) throws IOException, CancelledException {
@ -930,7 +927,7 @@ public class GhidraJarBuilder implements GhidraLaunchable {
System.exit(0);
}
/**
/*
* Entry point for 'gradle buildGhidraJar'.
*/
public static void main(String[] args) throws IOException {

View file

@ -1,3 +1,4 @@
MODULE FILE LICENSE: lib/commons-compress-1.21.jar Apache License 2.0
MODULE FILE LICENSE: lib/guava-31.1-jre.jar Apache License 2.0
MODULE FILE LICENSE: lib/failureaccess-1.0.1.jar Apache License 2.0
MODULE FILE LICENSE: lib/jdom-legacy-1.1.3.jar JDOM License

View file

@ -32,6 +32,7 @@ dependencies {
api "org.apache.logging.log4j:log4j-api:2.17.1"
api "org.apache.logging.log4j:log4j-core:2.17.1"
api "org.apache.commons:commons-collections4:4.1"
api "org.apache.commons:commons-compress:1.21"
api "org.apache.commons:commons-lang3:3.12.0"
api "org.apache.commons:commons-text:1.10.0"
api "commons-io:commons-io:2.11.0"

View file

@ -120,6 +120,8 @@ public class GenericApplicationLayout extends ApplicationLayout {
userTempDir = ApplicationUtilities.getDefaultUserTempDir(applicationProperties);
userSettingsDir = ApplicationUtilities.getDefaultUserSettingsDir(applicationProperties,
applicationInstallationDir);
extensionInstallationDirs = Collections.emptyList();
}
protected Collection<ResourceFile> getAdditionalApplicationRootDirs(

View file

@ -58,10 +58,43 @@ public class Json extends ToStringStyle {
}
}
/**
* A {@link ToStringStyle} inspired by {@link ToStringStyle#JSON_STYLE} that places
* object fields all on one line, with Json style formatting.
*/
public static class JsonWithFlatToStringStyle extends ToStringStyle {
private JsonWithFlatToStringStyle() {
this.setUseClassName(false);
this.setUseIdentityHashCode(false);
this.setContentStart("{ ");
this.setContentEnd(" }");
this.setArrayStart("[");
this.setArrayEnd("]");
this.setFieldSeparator(", ");
this.setFieldNameValueSeparator(": ");
this.setNullText("null");
this.setSummaryObjectStartText("\"<");
this.setSummaryObjectEndText(">\"");
this.setSizeStartText("\"<size=");
this.setSizeEndText(">\"");
}
}
/**
* Creates a Json string representation of the given object and all of its fields. To exclude
* some fields, call {@link #toStringExclude(Object, String...)}. To only include particular
* fields, call {@link #appendToString(StringBuffer, String)}.
* <p>
* The returned string is formatted for pretty printing using whitespace, such as tabs and
* newlines.
*
* @param o the object
* @return the string
*/
@ -69,6 +102,18 @@ public class Json extends ToStringStyle {
return ToStringBuilder.reflectionToString(o, Json.WITH_NEWLINES);
}
/**
* Creates a Json string representation of the given object and all of its fields.
* <p>
* The returned string is formatted without newlines for better use in logging.
*
* @param o the object
* @return the string
*/
public static String toStringFlat(Object o) {
return ToStringBuilder.reflectionToString(o, new JsonWithFlatToStringStyle());
}
/**
* Creates a Json string representation of the given object and the given fields
* @param o the object

View file

@ -215,6 +215,10 @@ public class Application {
return app.getModuleForClass(className);
}
public static ResourceFile getModuleContainingClass(Class<?> c) {
return app.getModuleForClass(c);
}
private void findJavaSourceDirectories(List<ResourceFile> list,
ResourceFile moduleRootDirectory) {
ResourceFile srcDir = new ResourceFile(moduleRootDirectory, "src");
@ -254,6 +258,18 @@ public class Application {
}
private ResourceFile getModuleForClass(String className) {
try {
Class<?> callersClass = Class.forName(className);
return getModuleForClass(callersClass);
}
catch (ClassNotFoundException e) {
// This can happen when we are being called from a script, which is not in the
// classpath. This file will not have a module anyway.
return null;
}
}
private String toPath(String className) {
// get rid of nested class name(s) if present
int dollar = className.indexOf('$');
if (dollar != -1) {
@ -261,27 +277,22 @@ public class Application {
}
String path = className.replace('.', '/');
String sourcePath = path + ".java";
String classFilePath = path + ".class";
return path + ".class";
}
private ResourceFile getModuleForClass(Class<?> clazz) {
if (inSingleJarMode()) {
String classFilePath = toPath(clazz.getName());
GModule gModule = getModuleFromTreeMap(classFilePath);
return gModule == null ? null : gModule.getModuleRoot();
}
// we're running from a binary installation...so get our jar and go up one
Class<?> callersClass;
try {
callersClass = Class.forName(className);
}
catch (ClassNotFoundException e) {
// This can happen when we are being called from a script, which is not in the
// classpath. This file will not have a module anyway
return null;
}
File sourceLocationForClass = SystemUtilities.getSourceLocationForClass(callersClass);
File sourceLocationForClass = SystemUtilities.getSourceLocationForClass(clazz);
if (sourceLocationForClass.isDirectory()) {
String classFilePath = toPath(clazz.getName());
String sourcePath = classFilePath.replace(".class", ".java");
return findModuleForJavaSource(sourcePath);
}

View file

@ -22,9 +22,12 @@ import java.util.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import generic.json.Json;
import ghidra.GhidraClassLoader;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.*;
import ghidra.util.task.TaskMonitor;
import utility.module.ModuleUtilities;
@ -34,6 +37,9 @@ import utility.module.ModuleUtilities;
public class ClassFinder {
static final Logger log = LogManager.getLogger(ClassFinder.class);
private static final boolean IS_USING_RESTRICTED_EXTENSIONS =
Boolean.getBoolean(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY);
private static List<Class<?>> FILTER_CLASSES =
Collections.unmodifiableList(Arrays.asList(ExtensionPoint.class));
@ -47,8 +53,10 @@ public class ClassFinder {
private void initialize(List<String> searchPaths, TaskMonitor monitor)
throws CancelledException {
Set<String> pathSet = new LinkedHashSet<>(searchPaths);
Msg.trace(this,
"Using restricted extension class loader? " + IS_USING_RESTRICTED_EXTENSIONS);
Set<String> pathSet = new LinkedHashSet<>(searchPaths);
Iterator<String> pathIterator = pathSet.iterator();
while (pathIterator.hasNext()) {
monitor.checkCancelled();
@ -110,26 +118,58 @@ public class ClassFinder {
return classList;
}
/*package*/ static Class<?> loadExtensionPoint(String path, String fullName) {
/**
* If the given class name matches the known extension name patterns, then this method will try
* to load that class using the provided path. Extensions may be loaded using their own
* class loader, depending on the system property
* {@link GhidraClassLoader#ENABLE_RESTRICTED_EXTENSIONS_PROPERTY}.
* <p>
* Examples:
* <pre>
* /foo/bar/baz/file.jar fully.qualified.ClassName
* /foo/bar/baz/bin fully.qualified.ClassName
* </pre>
*
* @param path the jar or dir path
* @param className the fully qualified class name
* @return the class if it is an extension point
*/
/*package*/ static Class<?> loadExtensionPoint(String path, String className) {
if (!ClassSearcher.isExtensionPointName(fullName)) {
if (!ClassSearcher.isExtensionPointName(className)) {
return null;
}
ClassLoader classLoader = ClassSearcher.class.getClassLoader();
ClassLoader classLoader = getClassLoader(path);
try {
Class<?> c = Class.forName(fullName, true, classLoader);
Class<?> c = Class.forName(className, true, classLoader);
if (isClassOfInterest(c)) {
return c;
}
}
catch (Throwable t) {
processClassLoadError(path, fullName, t);
processClassLoadError(path, className, t);
}
return null;
}
private static ClassLoader getClassLoader(String path) {
ClassLoader classLoader = ClassSearcher.class.getClassLoader();
if (!IS_USING_RESTRICTED_EXTENSIONS) {
return classLoader; // custom extension class loader is disabled
}
ExtensionDetails extension = ExtensionUtils.getExtension(path);
if (extension != null) {
Msg.trace(ClassFinder.class,
"Installing custom extension class loader for: " + Json.toStringFlat(extension));
classLoader = new ExtensionModuleClassLoader(extension);
}
return classLoader;
}
private static void processClassLoadError(String path, String name, Throwable t) {
if (t instanceof LinkageError) {

View file

@ -26,10 +26,12 @@ import java.util.stream.Collectors;
import javax.swing.event.ChangeListener;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import generic.jar.ResourceFile;
import ghidra.GhidraClassLoader;
import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
@ -277,15 +279,30 @@ public class ClassSearcher {
}
private static List<String> gatherSearchPaths() {
String cp = System.getProperty("java.class.path");
StringTokenizer st = new StringTokenizer(cp, File.pathSeparator);
//
// By default all classes are found on the standard classpath. In the default mode, there
// are no values associated with the GhidraClassLoader.CP_EXT property. Alternatively,
// users can enable Extension classpath restriction. In this mode, any Extension module's
// jar files will *not* be on the standard classpath, but instead will be on CP_EXT.
//
List<String> rawPaths = new ArrayList<>();
while (st.hasMoreTokens()) {
rawPaths.add(st.nextToken());
getPropertyPaths(GhidraClassLoader.CP, rawPaths);
getPropertyPaths(GhidraClassLoader.CP_EXT, rawPaths);
return canonicalizePaths(rawPaths);
}
private static void getPropertyPaths(String property, List<String> results) {
String paths = System.getProperty(property);
Msg.trace(ClassSearcher.class, "Paths in " + property + ": " + paths);
if (StringUtils.isBlank(paths)) {
return;
}
List<String> canonicalPaths = canonicalizePaths(rawPaths);
return canonicalPaths;
StringTokenizer st = new StringTokenizer(paths, File.pathSeparator);
while (st.hasMoreTokens()) {
results.add(st.nextToken());
}
}
private static List<String> canonicalizePaths(Collection<String> paths) {

View file

@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.project.extensions;
package ghidra.util.extensions;
import java.io.File;
import java.util.List;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import generic.jar.ResourceFile;
import generic.json.Json;
@ -191,16 +193,41 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
this.version = version;
}
/**
* Returns URLs for all jar files living in the {extension dir}/lib directory for an installed
* extension.
*
* @return the URLs
*/
public Set<URL> getLibraries() {
if (!isInstalled()) {
return Collections.emptySet();
}
Set<File> jarFiles = new HashSet<>();
findJarFiles(new File(installDir, "lib"), jarFiles);
Set<URL> paths = new HashSet<>();
for (File jar : jarFiles) {
try {
URL jarUrl = jar.toURI().toURL();
paths.add(jarUrl);
}
catch (MalformedURLException e) {
continue;
}
}
return paths;
}
/**
* 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.
* the installation status of an extension. When a user marks an extension to be uninstalled via
* the UI, the only thing that is done is to remove this manifest file, which tells the tool to
* remove the entire extension directory on the next launch.
*
* @return true if the extension is installed.
*/
@ -329,7 +356,7 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
public boolean clearMarkForUninstall() {
if (installDir == null) {
Msg.error(ExtensionUtils.class,
Msg.error(this,
"Cannot restore extension; extension installation dir is missing for: " + name);
return false; // already marked as uninstalled
}
@ -373,4 +400,16 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
public String toString() {
return Json.toString(this);
}
private void findJarFiles(File dir, Set<File> jarFiles) {
File[] files = dir.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isFile() && f.getName().endsWith(".jar")) {
jarFiles.add(f);
}
}
}
}

View file

@ -0,0 +1,45 @@
/* ###
* 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 java.net.URL;
import java.net.URLClassLoader;
import java.util.Set;
/**
* A class loader used with Ghidra extensions.
*/
public class ExtensionModuleClassLoader extends URLClassLoader {
private ExtensionDetails extensionDir;
public ExtensionModuleClassLoader(ExtensionDetails extensionDir) {
// It is important that this class use the default GhidraClassLoader as its parent. This
// allows resolution of Ghidra classes from extensions.
super(getURLs(extensionDir), ExtensionModuleClassLoader.class.getClassLoader());
this.extensionDir = extensionDir;
}
private static URL[] getURLs(ExtensionDetails extensionDir) {
Set<URL> jars = extensionDir.getLibraries();
return jars.toArray(URL[]::new);
}
@Override
public String toString() {
return "Extension ClassLoader for " + extensionDir.getName();
}
}

View file

@ -13,15 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.framework.project.extensions;
package ghidra.util.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;
@ -29,40 +26,25 @@ 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.
* Utilities for finding 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.
* Extension searching is cached. Use {@link #reload()} to update the cache.
*/
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. */
/**
* 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";
@ -70,13 +52,15 @@ public class ExtensionUtils {
private static final Logger log = LogManager.getLogger(ExtensionUtils.class);
private static Extensions extensions;
/**
* Performs extension maintenance. This should be called at startup, before any plugins or
* extension points are loaded.
*/
public static void initializeExtensions() {
Extensions extensions = getAllInstalledExtensions();
extensions = getAllInstalledExtensions();
// delete any extensions marked for removal
extensions.cleanupExtensionsMarkedForRemoval();
@ -85,14 +69,52 @@ public class ExtensionUtils {
extensions.reportDuplicateExtensions();
}
public static ExtensionDetails getExtension(String path) {
File pathDir = new File(path);
Set<ExtensionDetails> installedExtensions = getActiveInstalledExtensions();
for (ExtensionDetails ext : installedExtensions) {
File installDir = ext.getInstallDir();
if (FileUtilities.isPathContainedWithin(installDir, pathDir)) {
return ext;
}
}
return null;
}
/**
* Gets all known extensions that have not been marked for removal.
* 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.
*
* @return set of installed extensions
* @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 ExtensionUtils.getExtension(file, true) != null;
}
public static boolean install(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;
}
public static Set<ExtensionDetails> getActiveInstalledExtensions() {
Extensions extensions = getAllInstalledExtensions();
return extensions.getActiveExtensions();
return getAllInstalledExtensions().getActiveExtensions();
}
/**
@ -102,15 +124,18 @@ public class ExtensionUtils {
* @return set of installed extensions
*/
public static Set<ExtensionDetails> getInstalledExtensions() {
Extensions extensions = getAllInstalledExtensions();
return extensions.get();
return getAllInstalledExtensions().get();
}
private static Extensions getAllInstalledExtensions() {
public static Extensions getAllInstalledExtensions() {
if (extensions != null) {
return extensions;
}
log.trace("Finding all installed extensions...");
Extensions extensions = new Extensions();
extensions = new Extensions(log);
// Find all extension.properties or extension.properties.uninstalled files in
// the install directory and create a ExtensionDetails object for each.
@ -146,6 +171,36 @@ public class ExtensionUtils {
return extensions;
}
public 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;
}
/**
* Clears any cached extensions and searches for extensions.
*/
public static void reload() {
log.trace("Clearing extensions cache");
extensions = null;
getAllInstalledExtensions();
}
/**
* Returns all archive extensions. These are all the extensions found in
* {@link ApplicationLayout#getExtensionArchiveDir}. This are added to an installation as
@ -172,36 +227,68 @@ public class ExtensionUtils {
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);
Set<ExtensionDetails> results = new HashSet<>();
findExtensionsInZips(archiveFiles, results);
findExtensionsInFolder(archiveDir.getFile(false), results);
return extensions;
return results;
}
public static ExtensionDetails createExtensionFromProperties(File file) {
try {
return tryToLoadExtensionFromProperties(file);
}
catch (IOException e) {
log.error("Error loading extension properties from " + file.getAbsolutePath(), e);
return null;
}
}
public 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 void findExtensionsInZips(ResourceFile[] archiveFiles,
Set<ExtensionDetails> extensions) {
Set<ExtensionDetails> results) {
for (ResourceFile file : archiveFiles) {
ExtensionDetails extension = createExtensionDetailsFromArchive(file);
ExtensionDetails extension = ExtensionUtils.createExtensionDetailsFromArchive(file);
if (extension == null) {
log.trace("Skipping archive file; not an extension: " + file);
continue;
}
if (extensions.contains(extension)) {
if (results.contains(extension)) {
log.error(
"Skipping extension \"" + extension.getName() + "\" found at " +
extension.getInstallPath() +
".\nArchived extension by that name already found.");
}
extensions.add(extension);
results.add(extension);
}
}
private static void findExtensionsInFolder(File dir, Set<ExtensionDetails> extensions) {
private static void findExtensionsInFolder(File dir, Set<ExtensionDetails> results) {
List<File> propFiles = findExtensionPropertyFiles(dir);
for (File propFile : propFiles) {
ExtensionDetails extension = createExtensionFromProperties(propFile);
ExtensionDetails extension = ExtensionUtils.createExtensionFromProperties(propFile);
if (extension == null) {
continue;
}
@ -211,300 +298,15 @@ public class ExtensionUtils {
File extDir = propFile.getParentFile();
extension.setArchivePath(extDir.getAbsolutePath());
if (extensions.contains(extension)) {
if (results.contains(extension)) {
log.error(
"Skipping duplicate extension \"" + extension.getName() + "\" found at " +
extension.getInstallPath());
}
extensions.add(extension);
results.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 extension 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;
}
Extensions extensions = getAllInstalledExtensions();
if (checkForConflictWithDevelopmentExtension(extension, extensions)) {
return false;
}
if (checkForDuplicateExtensions(extension, extensions)) {
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) {
String name = newExtension.getName();
log.trace("Checking for duplicate extensions for '" + name + "'");
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
log.trace("No matching extensions installed");
return false;
}
log.trace("Duplicate extensions found by name '" + name + "'");
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) {
String name = newExtension.getName();
log.trace("Checking for duplicate dev mode extensions for '" + name + "'");
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
log.trace("No matching extensions installed");
return false;
}
for (ExtensionDetails extension : matches) {
if (extension.isInstalledInInstallationFolder()) {
String message = "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();
log.trace(removeNewlines(message));
OkDialog.showError("Duplicate Extensions Found", message);
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) {
@ -537,36 +339,62 @@ public class ExtensionUtils {
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) {
private static ExtensionDetails tryToLoadExtensionFromProperties(File file) throws IOException {
if (file == null) {
log.error("Cannot check for extension zip; null file");
return false;
Properties props = new Properties();
try (InputStream in = new FileInputStream(file.getAbsolutePath())) {
props.load(in);
return createExtensionDetails(props);
}
}
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);
}
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 (file.isDirectory()) {
return false;
if (!entry.getName().endsWith(PROPERTIES_FILE_NAME)) {
return null;
}
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;
}
InputStream propFile = zipFile.getInputStream(entry);
Properties prop = new Properties();
prop.load(propFile);
return prop;
}
/**
@ -618,64 +446,36 @@ public class ExtensionUtils {
return null;
}
private static ExtensionDetails createExtensionDetailsFromArchive(ResourceFile resourceFile) {
/**
* 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) {
File file = resourceFile.getFile(false);
if (!isZip(file)) {
return null;
if (file == null) {
log.error("Cannot check for extension zip; null file");
return false;
}
try (ZipFile zipFile = new ZipFile(file)) {
Properties props = getProperties(zipFile);
if (props != null) {
ExtensionDetails extension = createExtensionDetails(props);
extension.setArchivePath(file.getAbsolutePath());
return extension;
}
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.error(
"Unable to read zip file to get extension properties: " + file, e);
log.trace("Unable to check if file is a zip file: " + file + ". " + e.getMessage());
return false;
}
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;
}
/**
@ -708,19 +508,6 @@ public class ExtensionUtils {
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.
@ -770,6 +557,19 @@ public class ExtensionUtils {
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;
}
private static void writeZipEntryToFile(ZipFile zFile, ZipArchiveEntry entry, File destination)
throws IOException {
try (OutputStream outputStream =
@ -798,63 +598,6 @@ public class ExtensionUtils {
}
}
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.
*
@ -895,107 +638,4 @@ public class ExtensionUtils {
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());
}
String getAsString() {
StringBuilder buffy = new StringBuilder();
Set<Entry<String, List<ExtensionDetails>>> entries = extensionsByName.entrySet();
for (Entry<String, List<ExtensionDetails>> entry : entries) {
String name = entry.getKey();
buffy.append("Name: ").append(name);
List<ExtensionDetails> extensions = entry.getValue();
if (extensions.size() == 1) {
buffy.append(" - ").append(extensions.get(0).getInstallDir()).append('\n');
}
else {
for (ExtensionDetails e : extensions) {
buffy.append("\t").append(e.getInstallDir()).append('\n');
}
}
}
if (buffy.isEmpty()) {
return "<no extensions installed>";
}
if (!buffy.isEmpty()) {
// remove trailing newline to keep logging consistent
buffy.deleteCharAt(buffy.length() - 1);
}
return buffy.toString();
}
}
}

View file

@ -0,0 +1,195 @@
/* ###
* 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 java.io.File;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.logging.log4j.Logger;
import utilities.util.FileUtilities;
/**
* A collection of all extensions found. This class provides methods processing duplicates and
* managing extensions marked for removal.
*/
public class Extensions {
private Logger log;
private Map<String, List<ExtensionDetails>> extensionsByName = new HashMap<>();
Extensions(Logger log) {
this.log = log;
}
/**
* Returns all extensions matching the given details
* @param e the extension details to match
* @return all matching extensions
*/
public List<ExtensionDetails> getMatchingExtensions(ExtensionDetails e) {
return extensionsByName.computeIfAbsent(e.getName(), name -> List.of());
}
/**
* Adds an extension to this collection of extensions
* @param e the extension
*/
void add(ExtensionDetails e) {
extensionsByName.computeIfAbsent(e.getName(), n -> new ArrayList<>()).add(e);
}
/**
* Returns all installed extensions that are not marked for uninstall
* @return all installed extensions that are not marked for uninstall
*/
Set<ExtensionDetails> getActiveExtensions() {
return extensionsByName.values()
.stream()
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.filter(ext -> !ext.isPendingUninstall())
.collect(Collectors.toSet());
}
/**
* Removes any extensions that have already been marked for removal. This should be called
* before any class loading has occurred.
*/
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);
}
}
}
private 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;
}
/**
* Returns all unique extensions (no duplicates) that the application is aware of
* @return the extensions
*/
Set<ExtensionDetails> get() {
return extensionsByName.values()
.stream()
.filter(list -> !list.isEmpty())
.map(list -> list.get(0))
.collect(Collectors.toSet());
}
/**
* Returns a string representation of this collection of extensions
* @return a string representation of this collection of extensions
*/
String getAsString() {
StringBuilder buffy = new StringBuilder();
Set<Entry<String, List<ExtensionDetails>>> entries = extensionsByName.entrySet();
for (Entry<String, List<ExtensionDetails>> entry : entries) {
String name = entry.getKey();
buffy.append("Name: ").append(name);
List<ExtensionDetails> extensions = entry.getValue();
if (extensions.size() == 1) {
buffy.append(" - ").append(extensions.get(0).getInstallDir()).append('\n');
}
else {
for (ExtensionDetails e : extensions) {
buffy.append("\t").append(e.getInstallDir()).append('\n');
}
}
}
if (buffy.isEmpty()) {
return "<no extensions installed>";
}
if (!buffy.isEmpty()) {
// remove trailing newline to keep logging consistent
buffy.deleteCharAt(buffy.length() - 1);
}
return buffy.toString();
}
/**
* Logs any duplicate extensions
*/
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);
}
}
private 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());
}
}
}

View file

@ -1,2 +1 @@
MODULE FILE LICENSE: lib/commons-compress-1.21.jar Apache License 2.0
MODULE FILE LICENSE: lib/xz-1.9.jar Public Domain

View file

@ -27,7 +27,6 @@ dependencies {
api project(':FileSystem')
testImplementation project(path: ':Generic', configuration: 'testArtifacts')
api "org.apache.commons:commons-compress:1.21"
api "org.tukaani:xz:1.9"
}

View file

@ -153,7 +153,7 @@ public class PluginDescription implements Comparable<PluginDescription> {
*/
public String getModuleName() {
if (moduleName == null) {
ResourceFile moduleDir = Application.getModuleContainingClass(pluginClass.getName());
ResourceFile moduleDir = Application.getModuleContainingClass(pluginClass);
moduleName = (moduleDir == null) ? "<No Module>" : moduleDir.getName();
}

View file

@ -16,6 +16,7 @@
package ghidra.framework.plugintool.util;
import java.lang.reflect.*;
import java.util.List;
import ghidra.framework.plugintool.*;
import ghidra.util.Msg;
@ -76,6 +77,14 @@ public class PluginUtils {
*/
public static Class<? extends Plugin> forName(String pluginClassName) throws PluginException {
try {
List<Class<? extends Plugin>> classes = ClassSearcher.getClasses(Plugin.class);
for (Class<? extends Plugin> plug : classes) {
if (plug.getName().equals(pluginClassName)) {
return plug;
}
}
Class<?> tmpClass = Class.forName(pluginClassName);
if (!Plugin.class.isAssignableFrom(tmpClass)) {
throw new PluginException(
@ -84,7 +93,7 @@ public class PluginUtils {
return tmpClass.asSubclass(Plugin.class);
}
catch (ClassNotFoundException e) {
throw new PluginException("Plugin class not found");
throw new PluginException("Plugin class not found: " + pluginClassName);
}
}

View file

@ -23,6 +23,7 @@ import javax.swing.text.SimpleAttributeSet;
import docking.widgets.table.threaded.ThreadedTableModelListener;
import generic.theme.GColor;
import ghidra.framework.plugintool.dialog.AbstractDetailsPanel;
import ghidra.util.extensions.ExtensionDetails;
/**
* Panel that shows information about the selected extension in the {@link ExtensionTablePanel}. This

View file

@ -0,0 +1,276 @@
/* ###
* 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 java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
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.extensions.*;
import ghidra.util.task.TaskLauncher;
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 ExtensionInstaller {
private static final Logger log = LogManager.getLogger(ExtensionInstaller.class);
/**
* 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 extension file " + file);
if (file == null) {
log.error("Install file cannot be null");
return false;
}
ExtensionDetails extension = ExtensionUtils.getExtension(file, false);
if (extension == null) {
Msg.showError(ExtensionInstaller.class, null, "Error Installing Extension",
file.getAbsolutePath() + " does not point to a valid ghidra extension");
return false;
}
Extensions extensions = ExtensionUtils.getAllInstalledExtensions();
if (checkForConflictWithDevelopmentExtension(extension, extensions)) {
return false;
}
if (checkForDuplicateExtensions(extension, extensions)) {
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(ExtensionUtils.install(extension, file, monitor));
});
boolean success = installed.get();
if (success) {
log.trace("Finished installing " + file);
}
else {
log.trace("Failed to install " + file);
}
return success;
}
/**
* 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) {
String name = newExtension.getName();
log.trace("Checking for duplicate extensions for '" + name + "'");
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
log.trace("No matching extensions installed");
return false;
}
log.trace("Duplicate extensions found by name '" + name + "'");
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(ExtensionInstaller.class, null, "Duplicate Extensions Found",
buffy.toString());
}
private static boolean checkForConflictWithDevelopmentExtension(ExtensionDetails newExtension,
Extensions extensions) {
String name = newExtension.getName();
log.trace("Checking for duplicate dev mode extensions for '" + name + "'");
List<ExtensionDetails> matches = extensions.getMatchingExtensions(newExtension);
if (matches.isEmpty()) {
log.trace("No matching extensions installed");
return false;
}
for (ExtensionDetails extension : matches) {
if (extension.isInstalledInInstallationFolder()) {
String message = "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();
log.trace(removeNewlines(message));
OkDialog.showError("Duplicate Extensions Found", message);
return true;
}
}
return false;
}
}

View file

@ -27,6 +27,8 @@ import ghidra.framework.plugintool.ServiceProvider;
import ghidra.util.Msg;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.task.TaskMonitor;
@ -155,7 +157,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
// into this state is by clicking an extension that was discovered in the 'extension
// archives folder'
if (extension.isFromArchive()) {
if (ExtensionUtils.installExtensionFromArchive(extension)) {
if (ExtensionInstaller.installExtensionFromArchive(extension)) {
refreshTable();
}
return;
@ -192,6 +194,7 @@ class ExtensionTableModel extends ThreadedTableModel<ExtensionDetails, Object> {
return;
}
ExtensionUtils.reload();
Set<ExtensionDetails> archived = ExtensionUtils.getArchiveExtensions();
Set<ExtensionDetails> installed = ExtensionUtils.getInstalledExtensions();

View file

@ -27,6 +27,7 @@ import docking.widgets.table.*;
import ghidra.app.util.GenericHelpTopics;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
import ghidra.util.extensions.ExtensionDetails;
import help.Help;
import help.HelpService;

View file

@ -33,6 +33,7 @@ import ghidra.framework.Application;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
import resources.Icons;
@ -105,7 +106,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
super.dialogClosed();
if (extensionTablePanel.getTableModel().hasModelChanged() || requireRestart) {
Msg.showInfo(this, getComponent(), "Extensions Changed!",
Msg.showInfo(this, null, "Extensions Changed!",
"Please restart Ghidra for extension changes to take effect.");
}
}
@ -176,7 +177,7 @@ public class ExtensionTableProvider extends DialogComponentProvider {
continue;
}
boolean success = ExtensionUtils.install(file);
boolean success = ExtensionInstaller.install(file);
didInstall |= success;
}
return didInstall;

View file

@ -16,7 +16,6 @@
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.*;
@ -29,10 +28,10 @@ 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.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import ghidra.util.xml.XmlUtilities;
import utilities.util.FileUtilities;
@ -191,12 +190,7 @@ class ExtensionManager {
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);
Set<Class<?>> classes = findPluginsLoadedFromExtension(extension, pluginPaths);
extensionPlugins.addAll(classes);
}
@ -219,28 +213,31 @@ class ExtensionManager {
* 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 extension the extension from which to find plugins
* @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,
private static Set<Class<?>> findPluginsLoadedFromExtension(ExtensionDetails extension,
Set<PluginPath> pluginPaths) {
Set<Class<?>> result = new HashSet<>();
if (!extension.isInstalled()) {
return Collections.emptySet();
}
// Find any jar files in the directory provided
Set<String> jarPaths = getJarPaths(dir);
Set<URL> jarPaths = extension.getLibraries();
// Now get all Plugin.class file paths and see if any of them were loaded from one of the
// extension the given extension directory
Set<Class<?>> result = new HashSet<>();
for (PluginPath pluginPath : pluginPaths) {
if (pluginPath.isFrom(dir)) {
if (pluginPath.isFrom(extension.getInstallDir())) {
result.add(pluginPath.getPluginClass());
continue;
}
for (String jarPath : jarPaths) {
if (pluginPath.isFrom(jarPath)) {
for (URL jarUrl : jarPaths) {
if (pluginPath.isFrom(jarUrl)) {
result.add(pluginPath.getPluginClass());
}
}
@ -248,45 +245,6 @@ class ExtensionManager {
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;
@ -304,7 +262,8 @@ class ExtensionManager {
return FileUtilities.isPathContainedWithin(dir, pluginFile);
}
boolean isFrom(String jarPath) {
boolean isFrom(URL jarUrl) {
String jarPath = jarUrl.getPath();
return pluginLocation.contains(jarPath);
}

View file

@ -31,15 +31,17 @@ import docking.DialogComponentProvider;
import docking.test.AbstractDockingTest;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.extensions.ExtensionUtils;
import utilities.util.FileUtilities;
import utility.application.ApplicationLayout;
import utility.function.ExceptionalCallback;
import utility.module.ModuleUtilities;
/**
* Tests for the {@link ExtensionUtils} class.
* Tests for the {@link ExtensionInstaller} class.
*/
public class ExtensionUtilsTest extends AbstractDockingTest {
public class ExtensionInstallerTest extends AbstractDockingTest {
private static final String BUILD_FOLDER_NAME = "TestExtensionParentDir";
private static final String TEST_EXT_NAME = "test";
@ -87,7 +89,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
// Create an extension and install it.
File file = createExtensionZip(TEST_EXT_NAME);
ExtensionUtils.install(file);
ExtensionInstaller.install(file);
// Verify there is something in the installation directory and it has the correct name
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
@ -101,7 +103,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
// Create an extension and install it.
File file = createExtensionFolderInArchiveDir();
ExtensionUtils.install(file);
ExtensionInstaller.install(file);
// Verify the extension is in the install folder and has the correct name
checkExtensionInstalledInFilesystem(TEST_EXT_NAME);
@ -142,10 +144,9 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
@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));
assertFalse(ExtensionInstaller.install(new File("this/file/does/not/exist")));
assertFalse(ExtensionInstaller.install(null));
assertFalse(ExtensionInstaller.installExtensionFromArchive(null));
});
}
@ -156,7 +157,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
extension.setArchivePath(zipFile.getAbsolutePath());
String ghidraVersion = Application.getApplicationVersion();
extension.setVersion(ghidraVersion);
assertTrue(ExtensionUtils.installExtensionFromArchive(extension));
assertTrue(ExtensionInstaller.installExtensionFromArchive(extension));
}
@Test
@ -168,7 +169,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
didInstall.set(ExtensionInstaller.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
@ -187,7 +188,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
didInstall.set(ExtensionInstaller.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
@ -207,7 +208,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.installExtensionFromArchive(extension));
didInstall.set(ExtensionInstaller.installExtensionFromArchive(extension));
});
DialogComponentProvider confirmDialog =
@ -221,7 +222,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testMarkForUninstall_ClearMark() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
@ -238,7 +239,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testCleanupUninstalledExtions_WithExtensionMarkedForUninstall() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(TEST_EXT_NAME);
@ -255,8 +256,8 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testCleanupUninstalledExtions_SomeExtensionMarkedForUninstall() throws Exception {
List<File> extensionFolders = createTwoExternalExtensionsInFolder();
assertTrue(ExtensionUtils.install(extensionFolders.get(0)));
assertTrue(ExtensionUtils.install(extensionFolders.get(1)));
assertTrue(ExtensionInstaller.install(extensionFolders.get(0)));
assertTrue(ExtensionInstaller.install(extensionFolders.get(1)));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 2);
@ -279,7 +280,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
public void testCleanupUninstalledExtions_NoExtensionsMarkedForUninstall() throws Exception {
File externalFolder = createExternalExtensionInFolder();
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
assertExtensionInstalled(TEST_EXT_NAME);
// This should not uninstall any extensions
@ -299,7 +300,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
assertTrue(ExtensionInstaller.install(extensionFolder));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 1);
@ -313,7 +314,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
@ -329,7 +330,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
checkCleanInstall();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
// no longer an installed extension conflict; now we have a version mismatch
@ -349,7 +350,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
assertTrue(ExtensionInstaller.install(extensionFolder));
// create another extension Foo v2
File buildFolder2 = createTempDirectory("TestExtensionParentDir2");
@ -359,7 +360,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
@ -379,7 +380,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String appVersion = Application.getApplicationVersion();
File extensionFolder =
doCreateExternalExtensionInFolder(buildFolder, TEST_EXT_NAME, appVersion);
assertTrue(ExtensionUtils.install(extensionFolder));
assertTrue(ExtensionInstaller.install(extensionFolder));
Set<ExtensionDetails> extensions = ExtensionUtils.getInstalledExtensions();
assertEquals(extensions.size(), 1);
@ -393,7 +394,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
AtomicBoolean didInstall = new AtomicBoolean();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
DialogComponentProvider confirmDialog =
@ -409,7 +410,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
checkCleanInstall();
runSwingLater(() -> {
didInstall.set(ExtensionUtils.install(extensionFolder2));
didInstall.set(ExtensionInstaller.install(extensionFolder2));
});
waitFor(didInstall);
@ -437,7 +438,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
errorsExpected(() -> {
File zipFile = createZipWithMultipleExtensions();
assertFalse(ExtensionUtils.install(zipFile));
assertFalse(ExtensionInstaller.install(zipFile));
});
}
@ -452,7 +453,7 @@ public class ExtensionUtilsTest extends AbstractDockingTest {
String nameProperty = "ExtensionNamedFoo";
File externalFolder = createExtensionWithMismatchingNamePropertyString(nameProperty);
assertTrue(ExtensionUtils.install(externalFolder));
assertTrue(ExtensionInstaller.install(externalFolder));
ExtensionDetails extension = assertExtensionInstalled(nameProperty);

View file

@ -31,8 +31,23 @@ import ghidra.util.Msg;
*
*/
public class GhidraClassLoader extends URLClassLoader {
private static final String CP = "java.class.path";
/**
* When 'true', this property will trigger the system to put each Extension module's lib jar
* files into the {@link #CP_EXT} property.
*/
public static final String ENABLE_RESTRICTED_EXTENSIONS_PROPERTY =
"ghidra.extensions.classpath.restricted";
/**
* The classpath system property: {@code java.class.path}
*/
public static final String CP = "java.class.path";
/**
* The extensions classpath system property: {@code java.class.path.ext}
*/
public static final String CP_EXT = "java.class.path.ext";
/**
* This one-argument constructor is required for the JVM to successfully use this class loader
@ -45,7 +60,7 @@ public class GhidraClassLoader extends URLClassLoader {
}
@Override
public void addURL(URL url) {
public void addURL(URL url) {
super.addURL(url);
try {
System.setProperty(CP,

View file

@ -145,17 +145,36 @@ public class GhidraLauncher {
// First add Eclipse's module "bin" paths. If we didn't find any, assume Ghidra was
// compiled with Gradle, and add the module jars Gradle built.
addModuleBinPaths(classpathList, modules);
if (classpathList.isEmpty()) {
boolean gradleDevMode = classpathList.isEmpty();
if (gradleDevMode) {
// Add the module jars Gradle built.
// Note: this finds Extensions' jar files so there is no need to to call
// addExtensionJarPaths()
addModuleJarPaths(classpathList, modules);
}
else { /* Eclipse dev mode */
// Support loading pre-built, jar-based, non-repo extensions in Eclipse dev mode
addExtensionJarPaths(classpathList, modules, layout);
}
addExtensionJarPaths(classpathList, modules, layout);
// In development mode, jars do not live in module directories. Instead, each jar lives
// in an external, non-repo location, which is listed in build/libraryDependencies.txt.
addExternalJarPaths(classpathList, layout.getApplicationRootDirs());
}
else {
addPatchPaths(classpathList, layout.getPatchDir());
addModuleJarPaths(classpathList, modules);
}
//
// The framework may choose to handle extension class loading separately from all other
// class loading. In that case, we will separate the extension jar files from standard
// module jar files.
//
// (If the custom extension class loading is disabled, then the extensions will be put onto
// the standard classpath.)
setExtensionJarPaths(modules, layout, classpathList);
classpathList = orderClasspath(classpathList, modules);
return classpathList;
}
@ -202,6 +221,30 @@ public class GhidraLauncher {
dirs.forEach(d -> pathList.addAll(findJarsInDir(d)));
}
/**
* Initializes the Extension classpath system property, unless disabled.
* @param modules the known modules
* @param layout the application layout
* @param classpathList the standard classpath elements
*/
private static void setExtensionJarPaths(Map<String, GModule> modules,
GhidraApplicationLayout layout, List<String> classpathList) {
if (!Boolean.getBoolean(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY)) {
// custom extension class loader is disabled; use normal classpath
return;
}
List<String> extClasspathList = new ArrayList<>();
addExtensionJarPaths(extClasspathList, modules, layout);
// Remove the extensions that were added before this method was called
classpathList.removeAll(extClasspathList);
String extCp = String.join(File.pathSeparator, extClasspathList);
System.setProperty(GhidraClassLoader.CP_EXT, extCp);
}
/**
* 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

View file

@ -17,6 +17,7 @@ package utility.application;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Collections;
import generic.jar.ResourceFile;
import ghidra.framework.ApplicationProperties;
@ -30,7 +31,7 @@ public class DummyApplicationLayout extends ApplicationLayout {
/**
* Constructs a new dummy application layout object.
*
* @param name the application name
* @throws FileNotFoundException if there was a problem getting a user directory.
*/
public DummyApplicationLayout(String name) throws FileNotFoundException {
@ -48,5 +49,7 @@ public class DummyApplicationLayout extends ApplicationLayout {
// User directories
userTempDir = ApplicationUtilities.getDefaultUserTempDir(applicationProperties);
extensionInstallationDirs = Collections.emptyList();
}
}

View file

@ -102,6 +102,10 @@ VMARGS=-Xshare:off
# Limit on XML parsing. See https://docs.oracle.com/javase/tutorial/jaxp/limits/limits.html
#VMARGS=-Djdk.xml.totalEntitySizeLimit=50000000
# Restrict extensions to their own 'lib' directory for loading non-Ghidra jars. This may be used
# to fix class resolution if multiple extensions include different versions of the same named class.
#VMARGS=-Dghidra.extensions.classpath.restricted=true
# Enables PDB debug logging during import and analysis to .ghidra/.ghidra_ver/pdb.analyzer.log
#VMARGS=-Dghidra.pdb.logging=true

View file

@ -35,13 +35,13 @@ import docking.wizard.WizardManager;
import docking.wizard.WizardPanel;
import generic.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.archive.RestoreDialog;
import ghidra.framework.data.GhidraFileData;
import ghidra.framework.data.DefaultProjectData;
import ghidra.framework.data.GhidraFileData;
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.project.extensions.ExtensionTablePanel;
import ghidra.framework.project.extensions.ExtensionTableProvider;
import ghidra.framework.remote.User;
import ghidra.framework.store.LockException;
import ghidra.program.database.ProgramContentHandler;
@ -50,6 +50,7 @@ import ghidra.test.ProjectTestUtils;
import ghidra.util.InvalidNameException;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.ExtensionDetails;
import ghidra.util.task.TaskMonitor;
import resources.MultiIcon;
@ -703,7 +704,7 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
Language language = getZ80_LANGUAGE();
DomainFile otherFile =
ProjectTestUtils.createProgramFile(otherProject, "Program1", language,
language.getDefaultCompilerSpec(), null);
language.getDefaultCompilerSpec(), null);
ProjectTestUtils.createProgramFile(otherProject, "Program2", language,
language.getDefaultCompilerSpec(), null);