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
@@ -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;
/**
@@ -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*");
@@ -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 {
+1
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
+1
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"
@@ -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(
@@ -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
@@ -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);
}
@@ -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) {
@@ -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) {
@@ -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);
}
}
}
}
@@ -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();
}
}
@@ -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());
}
}
}
-1
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
-1
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"
}
@@ -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();
}
@@ -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);
}
}
@@ -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
@@ -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;
}
}
@@ -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();
@@ -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;
@@ -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;
@@ -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);
}
@@ -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);
@@ -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,
@@ -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
@@ -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();
}
}
@@ -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
@@ -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);