mirror of
https://github.com/NationalSecurityAgency/ghidra
synced 2024-09-13 21:56:19 +00:00
GP-3623 - Extensions - Added an extension-specific class loader; moved ExtensionUtils to Generic
This commit is contained in:
parent
80d92aa32f
commit
0a520b08bd
|
@ -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,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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue