Merge remote-tracking branch 'origin/GP-4515_ryanmkurtz_classsearcher--SQUASHED'

This commit is contained in:
Ryan Kurtz 2024-04-18 13:33:30 -04:00
commit 6132ddc3d0
16 changed files with 546 additions and 487 deletions

View File

@ -47,6 +47,7 @@ public interface AutoMapSpec extends ExtensionPoint {
private Private() {
ClassSearcher.addChangeListener(classListener);
classesChanged(null);
}
private synchronized void classesChanged(ChangeEvent evt) {

View File

@ -45,6 +45,7 @@ public interface AutoReadMemorySpec extends ExtensionPoint {
private Private() {
ClassSearcher.addChangeListener(classListener);
classesChanged(null);
}
private synchronized void classesChanged(ChangeEvent evt) {

View File

@ -89,7 +89,7 @@ public class GhidraRun implements GhidraLaunchable {
String projectPath = processArguments(args);
openProject(projectPath);
log.info("Ghidra starup complete (" + GhidraLauncher.getMillisecondsFromLaunch() +
log.info("Ghidra startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() +
" ms)");
});
};

View File

@ -27,8 +27,7 @@ import generic.jar.ResourceFile;
import ghidra.GhidraClassLoader;
import ghidra.framework.Application;
import ghidra.util.Disposable;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.classfinder.ExtensionPoint;
import ghidra.util.classfinder.*;
/**
* A dialog that shows useful runtime information
@ -188,13 +187,32 @@ class RuntimeInfoProvider extends ReusableDialogComponentProvider {
* loaded.
*/
private void addExtensionPoints() {
Map<String, String> map = ClassSearcher.getClasses(ExtensionPoint.class)
JTabbedPane epTabbedPane = new JTabbedPane();
tabbedPane.add("Extension Points", epTabbedPane);
// Discovered Potential Extension Points
Map<String, String> map = ClassSearcher.getExtensionPointInfo()
.stream()
.collect(Collectors.toMap(e -> e.getName(),
e -> ClassSearcher.getExtensionPointName(e.getName())));
String name = "Extension Points";
tabbedPane.add(new MapTablePanel<String, String>(name, map, "Name", "Extension Point", 400,
true, plugin), name);
.collect(Collectors.toMap(ClassFileInfo::name, ClassFileInfo::path));
String name = "Extension Point Info (%d)".formatted(map.size());
epTabbedPane.add(
new MapTablePanel<String, String>(name, map, "Name", "Path", 400, true, plugin), name);
// Loaded Extension Points
map = ClassSearcher.getLoaded()
.stream()
.collect(Collectors.toMap(ClassFileInfo::name, ClassFileInfo::suffix));
name = "Loaded (%d)".formatted(map.size());
epTabbedPane.add(
new MapTablePanel<String, String>(name, map, "Name", "Type", 400, true, plugin), name);
// False Positive Extension Points
map = ClassSearcher.getFalsePositives()
.stream()
.collect(Collectors.toMap(ClassFileInfo::name, ClassFileInfo::suffix));
name = "False Positives (%d)".formatted(map.size());
epTabbedPane.add(
new MapTablePanel<String, String>(name, map, "Name", "Type", 400, true, plugin), name);
}
/**

View File

@ -28,6 +28,7 @@ import ghidra.framework.*;
import ghidra.framework.model.DomainFolder;
import ghidra.framework.protocol.ghidra.Handler;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.InvalidInputException;
/**
@ -119,6 +120,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
Msg.info(AnalyzeHeadless.class,
"Headless startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() + " ms)");
ClassSearcher.logStatistics();
// Do the headless processing
try {

View File

@ -82,9 +82,8 @@ public class FieldNavigator implements ButtonPressedListener, FieldMouseHandlerS
new HashMap<Class<?>, List<FieldMouseHandler>>();
// find all instances of AnnotatedString
List<FieldMouseHandlerExtension> instances =
ClassSearcher.getInstances(FieldMouseHandlerExtension.class);
for (FieldMouseHandlerExtension fieldMouseHandler : instances) {
List<FieldMouseHandler> instances = ClassSearcher.getInstances(FieldMouseHandler.class);
for (FieldMouseHandler fieldMouseHandler : instances) {
addHandler(map, fieldMouseHandler);
}

View File

@ -21,6 +21,7 @@ import java.io.File;
import java.io.IOException;
import java.util.*;
import org.apache.commons.collections4.BidiMap;
import org.junit.BeforeClass;
import docking.test.AbstractDockingTest;
@ -45,6 +46,7 @@ import ghidra.program.model.symbol.Namespace;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.util.*;
import ghidra.util.Msg;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.RollbackException;
import junit.framework.AssertionFailedError;
@ -608,29 +610,27 @@ public abstract class AbstractGhidraHeadlessIntegrationTest extends AbstractDock
ServiceManager serviceManager = (ServiceManager) getInstanceField("serviceMgr", tool);
List<Class<?>> extentions =
(List<Class<?>>) getInstanceField("extensionPoints", ClassSearcher.class);
Set<Class<?>> set = new HashSet<>(extentions);
Iterator<Class<?>> iterator = set.iterator();
while (iterator.hasNext()) {
Class<?> c = iterator.next();
if (service.isAssignableFrom(c)) {
iterator.remove();
T instance = tool.getService(service);
serviceManager.removeService(service, instance);
}
Map<String, Set<ClassFileInfo>> extensionPointSuffixToInfoMap =
(Map<String, Set<ClassFileInfo>>) getInstanceField("extensionPointSuffixToInfoMap",
ClassSearcher.class);
BidiMap<ClassFileInfo, Class<?>> loadedCache =
(BidiMap<ClassFileInfo, Class<?>>) getInstanceField("loadedCache", ClassSearcher.class);
String suffix = ClassSearcher.getExtensionPointSuffix(service.getSimpleName());
if (suffix != null) {
Set<ClassFileInfo> serviceSet = extensionPointSuffixToInfoMap.get(suffix);
assertNotNull(serviceSet);
serviceSet.clear();
ClassFileInfo info = new ClassFileInfo("", replacement.getClass().getName(), suffix);
serviceSet.add(info);
loadedCache.put(info, replacement.getClass());
}
T instance = tool.getService(service);
if (instance != null) {
serviceManager.removeService(service, instance);
}
set.add(replacement.getClass());
serviceManager.addService(service, replacement);
List<Class<?>> newExtensionPoints = new ArrayList<>(set);
setInstanceField("extensionPoints", ClassSearcher.class, newExtensionPoints);
}
//==================================================================================================

View File

@ -26,7 +26,6 @@ import generic.jar.*;
import ghidra.GhidraApplicationLayout;
import ghidra.GhidraLaunchable;
import ghidra.framework.*;
import ghidra.util.classfinder.ClassFinder;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
@ -617,7 +616,7 @@ public class GhidraJarBuilder implements GhidraLaunchable {
if (clazz == null) {
System.out.println("Couldn't load " + path);
}
else if (ClassFinder.isClassOfInterest(clazz)) {
else if (ClassSearcher.isClassOfInterest(clazz)) {
extensionPointClasses.add(clazz.getName());
}
}

View File

@ -19,12 +19,12 @@ import static org.junit.Assert.*;
import java.awt.Color;
import java.awt.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import javax.swing.JPanel;
import javax.swing.table.TableModel;
import org.apache.commons.collections4.BidiMap;
import org.junit.*;
import docking.ActionContext;
@ -44,6 +44,7 @@ import ghidra.program.model.listing.Program;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;
import ghidra.util.ColorUtils;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
@ -99,7 +100,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
// The old option's default value should not be applied to the new option
//
installAnalyzer(NotReplacingTestAnalyzerStub.class);
installAnalyzer(NotReplacingTestStubAnalyzer.class);
// install old options; the default value will not be used in the new option
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
@ -122,7 +123,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
// The old option's default value should not be applied to the new option
//
installAnalyzer(NotReplacingTestAnalyzerStub.class);
installAnalyzer(NotReplacingTestStubAnalyzer.class);
// install old options; the default value will not be used in the new option
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
@ -149,7 +150,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
// The old option's non-default value should be applied to the new option
//
installAnalyzer(UseOldValueTestAnalyzerStub.class);
installAnalyzer(UseOldValueTestStubAnalyzer.class);
// install old options; the default value will not be used in the new option
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
@ -175,7 +176,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
// new option has a non-default
//
installAnalyzer(UseOldValueTestAnalyzerStub.class);
installAnalyzer(UseOldValueTestStubAnalyzer.class);
// install old options; the default value will not be used in the new option
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
@ -205,7 +206,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
// has a different object type than the old option.
//
installAnalyzer(ConvertValueTypeTestAnalyzerStub.class);
installAnalyzer(ConvertValueTypeTestStubAnalyzer.class);
// install old options; the default value will not be used in the new option
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
@ -233,12 +234,12 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
private void installOldOptions(Object value) {
Options programAnalysisOptions = program.getOptions(Program.ANALYSIS_PROPERTIES);
Options options = programAnalysisOptions.getOptions(AbstractTestAnalyzerStub.NAME);
Options options = programAnalysisOptions.getOptions(AbstractTestStubAnalyzer.NAME);
AbstractOptions abstractOptions = (AbstractOptions) getInstanceField("options", options);
// this call creates an 'unregistered option'
String fullOptionName =
"Analyzers." + AbstractTestAnalyzerStub.NAME + '.' + OLD_OPTION_NAME;
"Analyzers." + AbstractTestStubAnalyzer.NAME + '.' + OLD_OPTION_NAME;
Option option = abstractOptions.getOption(fullOptionName, OptionType.getOptionType(value),
OLD_OPTION_DEFAULT_VALUE);
@ -249,16 +250,23 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
});
}
@SuppressWarnings("unchecked")
private void installAnalyzer(Class<? extends Analyzer> analyzer) {
@SuppressWarnings("unchecked")
List<Class<?>> extensions =
(List<Class<?>>) getInstanceField("extensionPoints", ClassSearcher.class);
Map<String, Set<ClassFileInfo>> extensionPointSuffixToInfoMap =
(Map<String, Set<ClassFileInfo>>) getInstanceField("extensionPointSuffixToInfoMap",
ClassSearcher.class);
BidiMap<ClassFileInfo, Class<?>> loadedCache =
(BidiMap<ClassFileInfo, Class<?>>) getInstanceField("loadedCache", ClassSearcher.class);
// remove any traces of previous test runs
extensions.removeIf(c -> c.getSimpleName().contains("TestAnalyzerStub"));
Set<ClassFileInfo> analyzerSet = extensionPointSuffixToInfoMap.get("Analyzer");
assertNotNull(analyzerSet);
extensions.add(analyzer);
analyzerSet.removeIf(c -> c.name().contains("TestStubAnalyzer"));
ClassFileInfo info = new ClassFileInfo("", analyzer.getName(), "Analyzer");
analyzerSet.add(info);
loadedCache.put(info, analyzer);
}
private AnalysisOptionsDialog invokeAnalysisDialog() {
@ -284,19 +292,19 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
private void changeNewOption(String newValue) {
Options options = program.getOptions(Program.ANALYSIS_PROPERTIES);
Options analyzerOptions = options.getOptions(AbstractTestAnalyzerStub.NAME);
Options analyzerOptions = options.getOptions(AbstractTestStubAnalyzer.NAME);
tx(program, () -> analyzerOptions.putObject(NEW_OPTION_NAME, newValue));
}
private void changeOldOption(String newValue) {
Options programAnalysisOptions = program.getOptions(Program.ANALYSIS_PROPERTIES);
Options options = programAnalysisOptions.getOptions(AbstractTestAnalyzerStub.NAME);
Options options = programAnalysisOptions.getOptions(AbstractTestStubAnalyzer.NAME);
AbstractOptions abstractOptions = (AbstractOptions) getInstanceField("options", options);
// this call creates an 'unregistered option'
String fullOptionName =
"Analyzers." + AbstractTestAnalyzerStub.NAME + '.' + OLD_OPTION_NAME;
"Analyzers." + AbstractTestStubAnalyzer.NAME + '.' + OLD_OPTION_NAME;
Option option = abstractOptions.getOption(fullOptionName,
OptionType.getOptionType(OLD_OPTION_DEFAULT_VALUE), OLD_OPTION_DEFAULT_VALUE);
@ -309,14 +317,14 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
private void assertOldValueRemoved() {
Options options = program.getOptions(Program.ANALYSIS_PROPERTIES);
Options analyzerOptions = options.getOptions(AbstractTestAnalyzerStub.NAME);
Options analyzerOptions = options.getOptions(AbstractTestStubAnalyzer.NAME);
assertFalse("Old option not removed", analyzerOptions.contains(OLD_OPTION_NAME));
}
private void assertOnlyNewOptionsInUi() {
// click our analyzer in the list of options
selectAnalyzer(AbstractTestAnalyzerStub.NAME);
selectAnalyzer(AbstractTestStubAnalyzer.NAME);
// get the panel of options
JPanel panel =
@ -340,7 +348,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
private void assertOptionValue(String optionName, Object defaultValue) {
Options options = program.getOptions(Program.ANALYSIS_PROPERTIES);
Options analyzerOptions = options.getOptions(AbstractTestAnalyzerStub.NAME);
Options analyzerOptions = options.getOptions(AbstractTestStubAnalyzer.NAME);
Object value = analyzerOptions.getObject(optionName, null);
assertEquals("Option value is not as expected for '" + optionName + "'", defaultValue,
value);
@ -387,7 +395,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
// Inner Classes
//==================================================================================================
public static abstract class AbstractTestAnalyzerStub implements Analyzer {
public static abstract class AbstractTestStubAnalyzer implements Analyzer {
protected AnalysisOptionsUpdater updater = new AnalysisOptionsUpdater();
@ -472,9 +480,9 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
}
}
public static class NotReplacingTestAnalyzerStub extends AbstractTestAnalyzerStub {
public static class NotReplacingTestStubAnalyzer extends AbstractTestStubAnalyzer {
public NotReplacingTestAnalyzerStub() {
public NotReplacingTestStubAnalyzer() {
super();
updater.registerReplacement(NEW_OPTION_NAME, OLD_OPTION_NAME, oldValue -> {
@ -485,9 +493,9 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
}
}
public static class UseOldValueTestAnalyzerStub extends AbstractTestAnalyzerStub {
public static class UseOldValueTestStubAnalyzer extends AbstractTestStubAnalyzer {
public UseOldValueTestAnalyzerStub() {
public UseOldValueTestStubAnalyzer() {
super();
updater.registerReplacement(NEW_OPTION_NAME, OLD_OPTION_NAME, oldValue -> {
@ -496,9 +504,9 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
}
}
public static class ConvertValueTypeTestAnalyzerStub extends AbstractTestAnalyzerStub {
public static class ConvertValueTypeTestStubAnalyzer extends AbstractTestStubAnalyzer {
public ConvertValueTypeTestAnalyzerStub() {
public ConvertValueTypeTestStubAnalyzer() {
super();
updater.registerReplacement(NEW_OPTION_NAME, OLD_OPTION_NAME, oldValue -> {

View File

@ -33,7 +33,7 @@ class ClassDir {
classPackage = new ClassPackage(dir, "", monitor);
}
void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException {
void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) throws CancelledException {
classPackage.getClasses(set, monitor);
}

View File

@ -0,0 +1,25 @@
/* ###
* 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.classfinder;
/**
* Information about a class file on disk
*
* @param path The path to the class file (or jar containing the class)
* @param name The name of the class (including package)
* @param suffix The class suffix (i.e., extension point type name)
*/
public record ClassFileInfo(String path, String name, String suffix) {}

View File

@ -1,253 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.classfinder;
import java.io.File;
import java.lang.reflect.Modifier;
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;
/**
* Finds extension classes in the classpath
*/
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));
private Set<ClassDir> classDirs = new HashSet<>();
private Set<ClassJar> classJars = new HashSet<>();
public ClassFinder(List<String> searchPaths, TaskMonitor monitor) throws CancelledException {
initialize(searchPaths, monitor);
}
private void initialize(List<String> searchPaths, TaskMonitor monitor)
throws CancelledException {
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();
String path = pathIterator.next();
String lcPath = path.toLowerCase();
File file = new File(path);
if ((lcPath.endsWith(".jar") || lcPath.endsWith(".zip")) && file.exists()) {
if (ClassJar.ignoreJar(lcPath)) {
log.trace("Ignoring jar file: {}", path);
continue;
}
log.trace("Searching jar file: {}", path);
classJars.add(new ClassJar(path, monitor));
}
else if (file.isDirectory()) {
log.trace("Searching classpath directory: {}", path);
classDirs.add(new ClassDir(path, monitor));
}
}
}
List<Class<?>> getClasses(TaskMonitor monitor) throws CancelledException {
Set<Class<?>> classSet = new HashSet<>();
for (ClassDir dir : classDirs) {
monitor.checkCancelled();
dir.getClasses(classSet, monitor);
}
for (ClassJar jar : classJars) {
monitor.checkCancelled();
jar.getClasses(classSet, monitor);
}
List<Class<?>> classList = new ArrayList<>(classSet);
Collections.sort(classList, (c1, c2) -> {
// Sort classes primarily by priority and secondarily by name
int p1 = ExtensionPointProperties.Util.getPriority(c1);
int p2 = ExtensionPointProperties.Util.getPriority(c2);
if (p1 > p2) {
return -1;
}
if (p1 < p2) {
return 1;
}
String n1 = c1.getName();
String n2 = c2.getName();
if (n1.equals(n2)) {
// Same priority and same package/class name....just arbitrarily choose one
return Integer.compare(c1.hashCode(), c2.hashCode());
}
return n1.compareTo(n2);
});
return classList;
}
/**
* 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(className)) {
return null;
}
ClassLoader classLoader = getClassLoader(path);
try {
Class<?> c = Class.forName(className, true, classLoader);
if (isClassOfInterest(c)) {
return c;
}
}
catch (Throwable 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) {
// We see this sometimes when loading classes that match our naming convention for
// extension points, but are actually extending 3rd party libraries. For now, do
// not make noise in the log for this case.
Msg.trace(ClassFinder.class,
"LinkageError loading class " + name + "; Incompatible class version? ", t);
return;
}
if (!(t instanceof ClassNotFoundException)) {
Msg.error(ClassFinder.class, "Error loading class " + name + " - " + t.getMessage(), t);
return;
}
processClassNotFoundExcepetion(path, name, (ClassNotFoundException) t);
}
private static void processClassNotFoundExcepetion(String path, String name,
ClassNotFoundException t) {
if (!isModuleEntryMissingFromClasspath(path)) {
// not sure if this can actually happen--it implies a half-built Eclipse issue
Msg.error(ClassFinder.class, "Error loading class " + name + " - " + t.getMessage(), t);
return;
}
// We have a special case: we know a module class was loaded, but it is not in our
// classpath. This can happen in Eclipse when we scan all modules, but the launcher does
// not include all modules.
if (SystemUtilities.isInTestingMode()) {
// ignore the error in testing mode, as many modules are not loaded for any given test
return;
}
Msg.error(ClassFinder.class,
"Module class is missing from the classpath.\n\tUpdate your launcher " +
"accordingly.\n\tModule: '" + path + "'\n\tClass: '" + name + "'");
}
private static boolean isModuleEntryMissingFromClasspath(String path) {
boolean inModule = ModuleUtilities.isInModule(path);
if (!inModule) {
return false;
}
String classPath = System.getProperty("java.class.path");
boolean inClassPath = classPath.contains(path);
return !inClassPath;
}
/**
* Checks to see if the given class is an extension point of interest.
*
* @param c The class to check.
* @return True if the given class is an extension point of interest; otherwise, false.
*/
public static boolean isClassOfInterest(Class<?> c) {
if (Modifier.isAbstract(c.getModifiers())) {
return false;
}
if (c.getEnclosingClass() != null && !Modifier.isStatic(c.getModifiers())) {
return false;
}
if (!Modifier.isPublic(c.getModifiers())) {
return false;
}
if (ExtensionPointProperties.Util.isExcluded(c)) {
return false;
}
for (Class<?> filterClasse : FILTER_CLASSES) {
if (filterClasse.isAssignableFrom(c)) {
return true;
}
}
return false;
}
}

View File

@ -36,7 +36,7 @@ import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import utility.application.ApplicationLayout;
class ClassJar extends ClassLocation {
class ClassJar implements ClassLocation {
/**
* Pattern for matching jar files in a module lib dir
@ -51,6 +51,7 @@ class ClassJar extends ClassLocation {
private static final String PATCH_DIR_PATH_FORWARD_SLASHED = getPatchDirPath();
private static final Set<String> USER_PLUGIN_PATHS = loadUserPluginPaths();
private Set<ClassFileInfo> classes = new HashSet<>();
private String path;
ClassJar(String path, TaskMonitor monitor) throws CancelledException {
@ -61,8 +62,7 @@ class ClassJar extends ClassLocation {
}
@Override
protected void getClasses(Set<Class<?>> set, TaskMonitor monitor) {
checkForDuplicates(set);
public void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) {
set.addAll(classes);
}
@ -174,9 +174,9 @@ class ClassJar extends ClassLocation {
name = name.substring(0, name.indexOf(CLASS_EXT));
name = name.replace('/', '.');
Class<?> c = ClassFinder.loadExtensionPoint(path, name);
if (c != null) {
classes.add(c);
String epName = ClassSearcher.getExtensionPointSuffix(name);
if (epName != null) {
classes.add(new ClassFileInfo(path, name, epName));
}
}

View File

@ -15,52 +15,17 @@
*/
package ghidra.util.classfinder;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Represents a place from which {@link Class}s can be obtained
*/
abstract class ClassLocation {
interface ClassLocation {
protected static final String CLASS_EXT = ".class";
public static final String CLASS_EXT = ".class";
protected final Logger log = LogManager.getLogger(getClass());
protected Set<Class<?>> classes = new HashSet<>();
protected abstract void getClasses(Set<Class<?>> set, TaskMonitor monitor)
throws CancelledException;
protected void checkForDuplicates(Set<Class<?>> existingClasses) {
for (Class<?> c : classes) {
// Note: our class and a matching class in 'existingClasses' will be '==' since the
// class loader loaded the class by name--it will always find the same class, in
// classpath order.
if (existingClasses.contains(c)) {
log.warn(() -> generateMessage(c));
}
}
}
private String generateMessage(Class<?> c) {
return String.format("Class defined in multiple locations: %s. Keeping class loaded " +
"from %s; ignoring class from %s", c, toLocation(c), this);
}
private String toLocation(Class<?> clazz) {
String name = clazz.getName();
String classAsPath = '/' + name.replace('.', '/') + ".class";
URL url = clazz.getResource(classAsPath);
String urlPath = url.getPath();
int index = urlPath.indexOf(classAsPath);
return urlPath.substring(0, index);
}
public void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) throws CancelledException;
}

View File

@ -23,7 +23,7 @@ import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
class ClassPackage extends ClassLocation {
class ClassPackage implements ClassLocation {
private static final FileFilter CLASS_FILTER =
pathname -> pathname.getName().endsWith(CLASS_EXT);
@ -32,6 +32,7 @@ class ClassPackage extends ClassLocation {
private File rootDir;
private File packageDir;
private String packageName;
private Set<ClassFileInfo> classes = new HashSet<>();
ClassPackage(File rootDir, String packageName, TaskMonitor monitor) throws CancelledException {
monitor.checkCancelled();
@ -47,9 +48,9 @@ class ClassPackage extends ClassLocation {
String path = rootDir.getAbsolutePath();
Set<String> allClassNames = getAllClassNames();
for (String className : allClassNames) {
Class<?> c = ClassFinder.loadExtensionPoint(path, className);
if (c != null) {
classes.add(c);
String epName = ClassSearcher.getExtensionPointSuffix(className);
if (epName != null) {
classes.add(new ClassFileInfo(path, className, epName));
}
}
}
@ -88,9 +89,7 @@ class ClassPackage extends ClassLocation {
}
@Override
protected void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException {
checkForDuplicates(set);
public void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) throws CancelledException {
set.addAll(classes);

View File

@ -18,7 +18,10 @@ package ghidra.util.classfinder;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.nio.file.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
@ -27,21 +30,25 @@ import java.util.stream.Collectors;
import javax.swing.event.ChangeListener;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import generic.jar.ResourceFile;
import generic.json.Json;
import ghidra.GhidraClassLoader;
import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.*;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.*;
import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities;
import utility.module.ModuleUtilities;
/**
* This class is a collection of static methods used to discover classes that implement a
@ -60,88 +67,171 @@ import utilities.util.FileUtilities;
*/
public class ClassSearcher {
// This provides a means for custom apps that do not use a module structure to search all jars
private static final Logger log = LogManager.getLogger(ClassSearcher.class);
/**
* This provides a means for custom apps that do not use a module structure to search all jars
*/
public static final String SEARCH_ALL_JARS_PROPERTY = "class.searcher.search.all.jars";
private static final String SEARCH_ALL_JARS_PROPERTY_VALUE =
System.getProperty(SEARCH_ALL_JARS_PROPERTY, Boolean.FALSE.toString());
static final boolean SEARCH_ALL_JARS = Boolean.parseBoolean(SEARCH_ALL_JARS_PROPERTY_VALUE);
static final boolean SEARCH_ALL_JARS = Boolean.getBoolean(SEARCH_ALL_JARS_PROPERTY);
static final Logger log = LogManager.getLogger(ClassSearcher.class);
private static ClassFinder searcher;
private static List<Class<?>> extensionPoints;
private static final boolean IS_USING_RESTRICTED_EXTENSIONS =
Boolean.getBoolean(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY);
private static List<Class<?>> FILTER_CLASSES = Arrays.asList(ExtensionPoint.class);
private static Pattern extensionPointSuffixPattern;
private static Map<String, Set<ClassFileInfo>> extensionPointSuffixToInfoMap;
private static BidiMap<ClassFileInfo, Class<?>> loadedCache = new DualHashBidiMap<>();
private static Set<ClassFileInfo> falsePositiveCache = new HashSet<>();
private static volatile boolean hasSearched;
private static volatile boolean isSearching;
private static WeakSet<ChangeListener> listenerList =
WeakDataStructureFactory.createCopyOnReadWeakSet();
private static Pattern extensionPointSuffixPattern;
private static volatile boolean hasSearched;
private static volatile boolean isSearching;
private static final ClassFilter DO_NOTHING_FILTER = c -> true;
/**
* Prevent class instantiation
*/
private ClassSearcher() {
// you cannot create one of these
// do nothing
}
/**
* Searches the classpath and updates the list of available classes which satisfies the
* internal class filter. When the search completes (and was not cancelled), any registered
* change listeners are notified.
*
* @param monitor the progress monitor for the search
* @throws CancelledException if the operation was cancelled
*/
public static void search(TaskMonitor monitor) throws CancelledException {
if (hasSearched) {
log.trace("Already searched for classes: using cached results");
return;
}
log.trace("Using restricted extension class loader? " + IS_USING_RESTRICTED_EXTENSIONS);
Instant start = Instant.now();
isSearching = true;
if (Application.inSingleJarMode()) {
log.trace("Single Jar Mode: using extensions from the jar file");
extensionPointSuffixToInfoMap = loadExtensionClassesFromJar();
}
else {
extensionPointSuffixToInfoMap = findClasses(monitor);
}
log.trace("Found extension classes {}", extensionPointSuffixToInfoMap);
if (extensionPointSuffixToInfoMap.isEmpty()) {
throw new AssertException("Unable to locate extension points!");
}
hasSearched = true;
isSearching = false;
Swing.runNow(() -> fireClassListChanged());
String finishedMessage =
"Class search complete (" + ChronoUnit.MILLIS.between(start, Instant.now()) + " ms)";
monitor.setMessage(finishedMessage);
log.info(finishedMessage);
}
/**
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes that implement or
* derive from the given class
* derive from the given ancestor class
*
* @param c the filter class
* @param ancestorClass the ancestor class
* @return set of classes that implement or extend T
*/
public static <T> List<Class<? extends T>> getClasses(Class<T> c) {
return getClasses(c, null);
public static <T> List<Class<? extends T>> getClasses(Class<T> ancestorClass) {
return getClasses(ancestorClass, null);
}
/**
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes that
* implement or derive from the given class
* implement or derive from the given ancestor class
*
* @param c the filter class
* @param ancestorClass the ancestor class
* @param classFilter A Predicate that tests class objects (that are already of type T)
* for further filtering, <code>null</code> is equivalent to "return true"
* for further filtering, {@code null} is equivalent to "return true"
* @return {@link ExtensionPointProperties#priority() priority-sorted} list of
* classes that implement or extend T and pass the filtering test performed by the
* predicate
*/
@SuppressWarnings("unchecked") // we checked the type of each use so we know the casts are safe
public static <T> List<Class<? extends T>> getClasses(Class<T> c,
public static <T> List<Class<? extends T>> getClasses(Class<T> ancestorClass,
Predicate<Class<? extends T>> classFilter) {
if (!hasSearched) {
return List.of();
}
if (isSearching) {
throw new IllegalStateException(
"Cannot call the getClasses() while the ClassSearcher is searching!");
}
List<Class<? extends T>> list = new ArrayList<>();
if (extensionPoints == null) {
return list;
String suffix = getExtensionPointSuffix(ancestorClass.getName());
if (suffix == null) {
return List.of();
}
for (Class<?> extensionPoint : extensionPoints) {
if (c.isAssignableFrom(extensionPoint) &&
(classFilter == null || classFilter.test((Class<T>) extensionPoint))) {
list.add((Class<? extends T>) extensionPoint);
List<Class<? extends T>> list = new ArrayList<>();
for (ClassFileInfo info : extensionPointSuffixToInfoMap.get(suffix)) {
if (falsePositiveCache.contains(info)) {
continue;
}
Class<?> c = loadedCache.get(info);
if (c == null) {
c = loadExtensionPoint(info.path(), info.name());
ClassFileInfo existing = loadedCache.getKey(c);
if (existing != null) {
log.info(
"Skipping load of class '%s' from '%s'. Already loaded from '%s'."
.formatted(info.name(), info.path(), existing.path()));
}
if (c == null) {
falsePositiveCache.add(info);
continue;
}
}
loadedCache.put(info, c);
if (ancestorClass.isAssignableFrom(c) &&
(classFilter == null || classFilter.test((Class<T>) c))) {
list.add((Class<? extends T>) c);
}
}
prioritizeClasses(list);
return list;
}
/**
* Gets all {@link ExtensionPointProperties#priority() priority-sorted} class instances that
* implement or derive from the given filter class
*
* @param c the filter class
* @return {@link ExtensionPointProperties#priority() priority-sorted} {@link List} of
* class instances that implement or extend T
*/
public static <T> List<T> getInstances(Class<T> c) {
return getInstances(c, DO_NOTHING_FILTER);
return getInstances(c, filter -> true);
}
/**
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes
* instances that implement or derive from the given class
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes instances that
* implement or derive from the given filter class and pass the given filter predicate
*
* @param c the filter class
* @param filter A Predicate that tests class objects (that are already of type T)
* for further filtering, <code>null</code> is equivalent to "return true"
* @return {@link ExtensionPointProperties#priority() priority-sorted} list of
* classes instances that implement or extend T and pass the filtering test performed by
* the predicate
* @param filter A filter predicate that tests class objects (that are already of type T).
* {@code null} is equivalent to "return true".
* @return {@link ExtensionPointProperties#priority() priority-sorted} {@link List} of class
* instances that implement or extend T and pass the filtering test performed by the predicate
*/
public static <T> List<T> getInstances(Class<T> c, ClassFilter filter) {
List<Class<? extends T>> classes = getClasses(c);
@ -214,91 +304,178 @@ public class ClassSearcher {
}
/**
* This deprecated method is now simply a pass-through for {@link #search(TaskMonitor)}.
* Gets class information about each discovered potential extension point.
* <p>
* NOTE: A discovered potential extension point may end up not getting loaded if it is not
* "of interest" (see {@link #isClassOfInterest(Class)}. These are referred to as false
* positives.
*
* @param forceRefresh ignored
* @param monitor the task monitor
* @throws CancelledException if cancelled
* @deprecated use {@link #search(TaskMonitor)} instead
* @return A {@link Set} of class information about each discovered potential extension point
*/
@Deprecated(forRemoval = true, since = "10.1") // remove 2 releases after 10.1
public static void search(boolean forceRefresh, TaskMonitor monitor)
throws CancelledException {
search(monitor);
public static Set<ClassFileInfo> getExtensionPointInfo() {
return extensionPointSuffixToInfoMap.values()
.stream()
.flatMap(e -> e.stream())
.collect(Collectors.toSet());
}
/**
* Searches the classpath and updates the list of available classes which
* satisfy the class filter. Classes which
* data types, and language providers. When the search completes and was
* not cancelled, the change listeners are notified.
*
* @param monitor the progress monitor for the search.
* @throws CancelledException if the operation is cancelled
* Gets class information about each loaded extension point.
* <p>
* NOTE: Ghidra may load more classes as it runs. Therefore, repeated calls to this method may
* return more results, as more extension points are loaded.
*
* @return A {@link Set} of class information about each loaded extension point
*/
public static void search(TaskMonitor monitor) throws CancelledException {
if (hasSearched) {
log.trace("Already searched for classes: using cached results");
return;
}
if (Application.inSingleJarMode()) {
log.trace("Single Jar Mode: using extensions from the jar file");
loadExtensionClassesFromJar();
return;
}
isSearching = true;
loadExtensionPointSuffixes();
extensionPoints = null;
long t = (new Date()).getTime();
log.info("Searching for classes...");
List<String> searchPaths = gatherSearchPaths();
searcher = new ClassFinder(searchPaths, monitor);
monitor.setMessage("Loading classes...");
extensionPoints = searcher.getClasses(monitor);
log.trace("Found extension classes {}", extensionPoints);
if (extensionPoints.isEmpty()) {
throw new AssertException("Unable to location extension points!");
}
hasSearched = true;
isSearching = false;
SystemUtilities.runSwingNow(() -> fireClassListChanged());
String finishedMessage = "Class search complete (%d ms, %d classes loaded)"
.formatted((new Date()).getTime() - t, extensionPoints.size());
monitor.setMessage(finishedMessage);
log.info(finishedMessage);
public static Set<ClassFileInfo> getLoaded() {
return loadedCache.keySet();
}
/**
* Gets the given class's extension point name
* Gets class information about discovered potential extension points that end up not getting
* loaded.
* <p>
* NOTE: Ghidra may load more classes as it runs. Therefore, repeated calls to this method may
* return more results, as more potential extension points are identified as false positives.
*
* @return A {@link Set} of class information about each loaded extension point
*/
public static Set<ClassFileInfo> getFalsePositives() {
return falsePositiveCache;
}
/**
* Gets the given class's extension point suffix.
* <p>
* Note that if multiple suffixes match, the smallest will be chosen. For a detailed
* explanation, see the comment inside {@link #loadExtensionPointSuffixes()}.
*
* @param className The name of the potential extension point class
* @return The given class's extension point name, or null if it is not an extension point
* @return The given class's extension point suffix, or null if it is not an extension point or
* {@link #search(TaskMonitor)} has not been called yet
*/
public static String getExtensionPointName(String className) {
if (className.indexOf("Test$") > 0 || className.endsWith("Test")) {
public static String getExtensionPointSuffix(String className) {
if (extensionPointSuffixPattern == null) {
extensionPointSuffixPattern = loadExtensionPointSuffixes();
}
if (className.contains("$") || className.endsWith("Test")) {
return null;
}
int packageIndex = className.lastIndexOf('.');
int innerClassIndex = className.lastIndexOf('$');
int maximumIndex = StrictMath.max(packageIndex, innerClassIndex);
if (maximumIndex > 0) {
className = className.substring(maximumIndex + 1);
if (packageIndex > 0) {
className = className.substring(packageIndex + 1);
}
Matcher m = extensionPointSuffixPattern.matcher(className);
return m.find() && m.groupCount() == 1 ? m.group(1) : null;
}
/**
* Checks to see if the given class is an extension point of interest.
*
* @param c The class to check.
* @return True if the given class is an extension point of interest; otherwise, false.
*/
public static boolean isClassOfInterest(Class<?> c) {
if (Modifier.isAbstract(c.getModifiers())) { // we don't support abstract (includes interfaces)
return false;
}
if (c.getEnclosingClass() != null) { // we don't support inner classes
return false;
}
if (!Modifier.isPublic(c.getModifiers())) { // we don't support non-public
return false;
}
if (ExtensionPointProperties.Util.isExcluded(c)) {
return false;
}
for (Class<?> filterClass : FILTER_CLASSES) {
if (filterClass.isAssignableFrom(c)) {
return true;
}
}
return false;
}
/**
* Writes the current class searcher statistics to the info log
*/
public static void logStatistics() {
log.info("Class searcher loaded %d extension points (%d false positives)"
.formatted(loadedCache.size(), falsePositiveCache.size()));
}
/**
* Scans the disk to find potential extension point class files. Matching is performed by file
* name only. The class files are not opened or loaded by this method.
*
* @param monitor the progress monitor for the disk scan
* @return A {@link Map} of discovered {@link ClassFileInfo class information}, keyed by their
* extension point suffix
* @throws CancelledException if the user cancelled the operation
*/
private static Map<String, Set<ClassFileInfo>> findClasses(TaskMonitor monitor)
throws CancelledException {
log.info("Searching for classes...");
Set<ClassDir> classDirs = new HashSet<>();
Set<ClassJar> classJars = new HashSet<>();
for (String searchPath : gatherSearchPaths()) {
String lcSearchPath = searchPath.toLowerCase();
File searchFile = new File(searchPath);
if ((lcSearchPath.endsWith(".jar") || lcSearchPath.endsWith(".zip")) &&
searchFile.exists()) {
if (ClassJar.ignoreJar(searchPath)) {
log.trace("Ignoring jar file: {}", searchPath);
continue;
}
log.trace("Searching jar file: {}", searchPath);
classJars.add(new ClassJar(searchPath, monitor));
}
else if (searchFile.isDirectory()) {
log.trace("Searching classpath directory: {}", searchPath);
classDirs.add(new ClassDir(searchPath, monitor));
}
}
Set<ClassFileInfo> classSet = new HashSet<>();
for (ClassDir dir : classDirs) {
monitor.checkCancelled();
dir.getClasses(classSet, monitor);
}
for (ClassJar jar : classJars) {
monitor.checkCancelled();
jar.getClasses(classSet, monitor);
}
return classSet.stream()
.collect(Collectors.groupingBy(ClassFileInfo::suffix, Collectors.toSet()));
}
/**
* Sorts the given {@link List} of {@link Class}es first by their defined priority, then by
* their name.
*
* @param list The {@link List} of {@link Class}es to sort. This {@link List} will be modified.
*/
private static <T> void prioritizeClasses(List<Class<? extends T>> list) {
Collections.sort(list, (c1, c2) -> {
// Sort classes primarily by priority and secondarily by name
int p1 = ExtensionPointProperties.Util.getPriority(c1);
int p2 = ExtensionPointProperties.Util.getPriority(c2);
if (p1 > p2) {
return -1;
}
if (p1 < p2) {
return 1;
}
return c1.getName().compareTo(c2.getName());
});
}
private static List<String> gatherSearchPaths() {
//
@ -315,7 +492,7 @@ public class ClassSearcher {
private static void getPropertyPaths(String property, List<String> results) {
String paths = System.getProperty(property);
Msg.trace(ClassSearcher.class, "Paths in " + property + ": " + paths);
log.trace("Paths in {}: {}", property, paths);
if (StringUtils.isBlank(paths)) {
return;
}
@ -351,36 +528,35 @@ public class ClassSearcher {
catch (InvalidPathException e) {
// we have seen odd strings being placed into the classpath--ignore them, as we
// don't know how to use them
Msg.trace(ClassSearcher.class, "Invalid path '" + path + "'", e);
log.trace("Invalid path '{}'", path);
return path;
}
}
private static void loadExtensionClassesFromJar() {
private static Map<String, Set<ClassFileInfo>> loadExtensionClassesFromJar() {
ResourceFile appRoot = Application.getApplicationRootDirectory();
ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES");
try {
List<String> classNames = FileUtilities.getLines(extensionClassesFile);
List<Class<?>> extensionClasses = new ArrayList<>();
Set<ClassFileInfo> extensionClasses = new HashSet<>();
for (String className : classNames) {
try {
Class<?> clazz = Class.forName(className);
extensionClasses.add(clazz);
}
catch (ClassNotFoundException e) {
Msg.warn(ClassSearcher.class, "Can't load extension point: " + className);
String epName = getExtensionPointSuffix(className);
if (epName != null) {
extensionClasses
.add(new ClassFileInfo(appRoot.getAbsolutePath(), className, epName));
}
}
extensionPoints = Collections.unmodifiableList(extensionClasses);
return extensionClasses.stream()
.collect(Collectors.groupingBy(ClassFileInfo::suffix, Collectors.toSet()));
}
catch (IOException e) {
throw new AssertException("Unexpected IOException reading extension class file " +
extensionClassesFile, e);
throw new AssertException(
"Unexpected IOException reading extension class file " + extensionClassesFile, e);
}
}
private static void loadExtensionPointSuffixes() {
private static Pattern loadExtensionPointSuffixes() {
Set<String> extensionPointSuffixes = new HashSet<>();
Collection<ResourceFile> moduleRootDirectories = Application.getModuleRootDirectories();
@ -397,6 +573,26 @@ public class ClassSearcher {
}
}
// Build regex of the form .*(suffix1|suffix2|suffix3|...)$
// If one suffix ends with another suffix, precedence should be given to the shorter one.
// This will result in some false positives, but will prevent some corner error cases as
// described in this example:
// There are 2 valid suffixes, Plugin and BobPlugin. Someone makes a new class:
//
// class BillBobPlugin extends Plugin
//
// The person who made this class was unaware that BobPlugin was also a valid suffix. If
// we were to match on the longest suffix, BillBobPlugin would erroneously be associated
// with BobPlugin, and getClasses() would fail. Now consider this example:
//
// class BillBobPlugin extends BobPlugin
//
// Since BillBobPlugin will be associated with the shorter "Plugin" suffix, it will be
// grouped with the other Plugin extension points. However, when getClasses(BobPlugin.class)
// is called, we retrieve the same shortest suffix from the given "BobPlugin" class name,
// which is Plugin. This will result in BillBobPlugin getting properly discovered from the
// Plugin group. Final checks are performed to make sure the provided class is assignable
// from any class in the group, which filters out the bad associations.
StringBuilder buffy = new StringBuilder(".*(");
String between = "";
for (String suffix : extensionPointSuffixes) {
@ -410,12 +606,8 @@ public class ClassSearcher {
between = "|";
}
buffy.append(")$");
extensionPointSuffixPattern = Pattern.compile(buffy.toString());
log.trace("Using extension point pattern: {}", extensionPointSuffixPattern);
}
static boolean isExtensionPointName(String name) {
return getExtensionPointName(name) != null;
log.trace("Using extension point pattern: {}", buffy);
return Pattern.compile(buffy.toString());
}
private static void fireClassListChanged() {
@ -429,4 +621,107 @@ public class ClassSearcher {
}
}
}
/**
* 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
*/
private static Class<?> loadExtensionPoint(String path, String className) {
if (getExtensionPointSuffix(className) == null) {
return null;
}
ClassLoader classLoader = getClassLoader(path);
try {
Class<?> c = Class.forName(className, true, classLoader);
if (isClassOfInterest(c)) {
return c;
}
}
catch (Throwable 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) {
log.trace(() -> "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) {
// We see this sometimes when loading classes that match our naming convention for
// extension points, but are actually extending 3rd party libraries. For now, do
// not make noise in the log for this case.
log.trace("LinkageError loading class {}; Incompatible class version? ", name, t);
return;
}
if (!(t instanceof ClassNotFoundException)) {
log.error("Error loading class {} - {}", name, t.getMessage(), t);
return;
}
processClassNotFoundExcepetion(path, name, (ClassNotFoundException) t);
}
private static void processClassNotFoundExcepetion(String path, String name,
ClassNotFoundException t) {
if (!isModuleEntryMissingFromClasspath(path)) {
// not sure if this can actually happen--it implies a half-built Eclipse issue
log.error("Error loading class {} - {}", name, t.getMessage(), t);
return;
}
// We have a special case: we know a module class was loaded, but it is not in our
// classpath. This can happen in Eclipse when we scan all modules, but the launcher does
// not include all modules.
if (SystemUtilities.isInTestingMode()) {
// ignore the error in testing mode, as many modules are not loaded for any given test
return;
}
log.error("Module class is missing from the classpath.\n\tUpdate your launcher " +
"accordingly.\n\tModule: '" + path + "'\n\tClass: '" + name + "'");
}
private static boolean isModuleEntryMissingFromClasspath(String path) {
boolean inModule = ModuleUtilities.isInModule(path);
if (!inModule) {
return false;
}
String classPath = System.getProperty("java.class.path");
boolean inClassPath = classPath.contains(path);
return !inClassPath;
}
}