GP-4515: Loading ExtensionPoints on-demand for faster startup

This commit is contained in:
Ryan Kurtz 2024-04-18 13:26:56 -04:00
parent 20f5bd9bec
commit 88c5d0a3fd
16 changed files with 546 additions and 487 deletions

View file

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

View file

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

View file

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

View file

@ -27,8 +27,7 @@ import generic.jar.ResourceFile;
import ghidra.GhidraClassLoader; import ghidra.GhidraClassLoader;
import ghidra.framework.Application; import ghidra.framework.Application;
import ghidra.util.Disposable; import ghidra.util.Disposable;
import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.*;
import ghidra.util.classfinder.ExtensionPoint;
/** /**
* A dialog that shows useful runtime information * A dialog that shows useful runtime information
@ -188,13 +187,32 @@ class RuntimeInfoProvider extends ReusableDialogComponentProvider {
* loaded. * loaded.
*/ */
private void addExtensionPoints() { 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() .stream()
.collect(Collectors.toMap(e -> e.getName(), .collect(Collectors.toMap(ClassFileInfo::name, ClassFileInfo::path));
e -> ClassSearcher.getExtensionPointName(e.getName()))); String name = "Extension Point Info (%d)".formatted(map.size());
String name = "Extension Points"; epTabbedPane.add(
tabbedPane.add(new MapTablePanel<String, String>(name, map, "Name", "Extension Point", 400, new MapTablePanel<String, String>(name, map, "Name", "Path", 400, true, plugin), name);
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.model.DomainFolder;
import ghidra.framework.protocol.ghidra.Handler; import ghidra.framework.protocol.ghidra.Handler;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.InvalidInputException; import ghidra.util.exception.InvalidInputException;
/** /**
@ -119,6 +120,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
Msg.info(AnalyzeHeadless.class, Msg.info(AnalyzeHeadless.class,
"Headless startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() + " ms)"); "Headless startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() + " ms)");
ClassSearcher.logStatistics();
// Do the headless processing // Do the headless processing
try { try {

View file

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

View file

@ -21,6 +21,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import org.apache.commons.collections4.BidiMap;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import docking.test.AbstractDockingTest; import docking.test.AbstractDockingTest;
@ -45,6 +46,7 @@ import ghidra.program.model.symbol.Namespace;
import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.Symbol;
import ghidra.program.util.*; import ghidra.program.util.*;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.classfinder.ClassFileInfo;
import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.RollbackException; import ghidra.util.exception.RollbackException;
import junit.framework.AssertionFailedError; import junit.framework.AssertionFailedError;
@ -608,29 +610,27 @@ public abstract class AbstractGhidraHeadlessIntegrationTest extends AbstractDock
ServiceManager serviceManager = (ServiceManager) getInstanceField("serviceMgr", tool); ServiceManager serviceManager = (ServiceManager) getInstanceField("serviceMgr", tool);
List<Class<?>> extentions = Map<String, Set<ClassFileInfo>> extensionPointSuffixToInfoMap =
(List<Class<?>>) getInstanceField("extensionPoints", ClassSearcher.class); (Map<String, Set<ClassFileInfo>>) getInstanceField("extensionPointSuffixToInfoMap",
Set<Class<?>> set = new HashSet<>(extentions); ClassSearcher.class);
Iterator<Class<?>> iterator = set.iterator(); BidiMap<ClassFileInfo, Class<?>> loadedCache =
while (iterator.hasNext()) { (BidiMap<ClassFileInfo, Class<?>>) getInstanceField("loadedCache", ClassSearcher.class);
Class<?> c = iterator.next(); String suffix = ClassSearcher.getExtensionPointSuffix(service.getSimpleName());
if (service.isAssignableFrom(c)) {
iterator.remove(); if (suffix != null) {
T instance = tool.getService(service); Set<ClassFileInfo> serviceSet = extensionPointSuffixToInfoMap.get(suffix);
serviceManager.removeService(service, instance); 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); T instance = tool.getService(service);
if (instance != null) { if (instance != null) {
serviceManager.removeService(service, instance); serviceManager.removeService(service, instance);
} }
set.add(replacement.getClass());
serviceManager.addService(service, replacement); 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.GhidraApplicationLayout;
import ghidra.GhidraLaunchable; import ghidra.GhidraLaunchable;
import ghidra.framework.*; import ghidra.framework.*;
import ghidra.util.classfinder.ClassFinder;
import ghidra.util.classfinder.ClassSearcher; import ghidra.util.classfinder.ClassSearcher;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
@ -617,7 +616,7 @@ public class GhidraJarBuilder implements GhidraLaunchable {
if (clazz == null) { if (clazz == null) {
System.out.println("Couldn't load " + path); System.out.println("Couldn't load " + path);
} }
else if (ClassFinder.isClassOfInterest(clazz)) { else if (ClassSearcher.isClassOfInterest(clazz)) {
extensionPointClasses.add(clazz.getName()); extensionPointClasses.add(clazz.getName());
} }
} }

View file

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

View file

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

View file

@ -15,52 +15,17 @@
*/ */
package ghidra.util.classfinder; package ghidra.util.classfinder;
import java.net.URL;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
/** /**
* Represents a place from which {@link Class}s can be obtained * 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()); public void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) throws CancelledException;
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);
}
} }

View file

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

View file

@ -18,7 +18,10 @@ package ghidra.util.classfinder;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.nio.file.*; import java.nio.file.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -27,21 +30,25 @@ import java.util.stream.Collectors;
import javax.swing.event.ChangeListener; 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.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import generic.json.Json;
import ghidra.GhidraClassLoader; import ghidra.GhidraClassLoader;
import ghidra.framework.Application; import ghidra.framework.Application;
import ghidra.util.Msg; import ghidra.util.*;
import ghidra.util.SystemUtilities;
import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet; import ghidra.util.datastruct.WeakSet;
import ghidra.util.exception.AssertException; import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.extensions.*;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
import utilities.util.FileUtilities; import utilities.util.FileUtilities;
import utility.module.ModuleUtilities;
/** /**
* This class is a collection of static methods used to discover classes that implement a * 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 { 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"; public static final String SEARCH_ALL_JARS_PROPERTY = "class.searcher.search.all.jars";
private static final String SEARCH_ALL_JARS_PROPERTY_VALUE = static final boolean SEARCH_ALL_JARS = Boolean.getBoolean(SEARCH_ALL_JARS_PROPERTY);
System.getProperty(SEARCH_ALL_JARS_PROPERTY, Boolean.FALSE.toString());
static final boolean SEARCH_ALL_JARS = Boolean.parseBoolean(SEARCH_ALL_JARS_PROPERTY_VALUE);
static final Logger log = LogManager.getLogger(ClassSearcher.class); private static final boolean IS_USING_RESTRICTED_EXTENSIONS =
Boolean.getBoolean(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY);
private static ClassFinder searcher;
private static List<Class<?>> extensionPoints;
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 = private static WeakSet<ChangeListener> listenerList =
WeakDataStructureFactory.createCopyOnReadWeakSet(); WeakDataStructureFactory.createCopyOnReadWeakSet();
private static Pattern extensionPointSuffixPattern; /**
* Prevent class instantiation
private static volatile boolean hasSearched; */
private static volatile boolean isSearching;
private static final ClassFilter DO_NOTHING_FILTER = c -> true;
private ClassSearcher() { 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 * 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 * @return set of classes that implement or extend T
*/ */
public static <T> List<Class<? extends T>> getClasses(Class<T> c) { public static <T> List<Class<? extends T>> getClasses(Class<T> ancestorClass) {
return getClasses(c, null); return getClasses(ancestorClass, null);
} }
/** /**
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes that * 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) * @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 * @return {@link ExtensionPointProperties#priority() priority-sorted} list of
* classes that implement or extend T and pass the filtering test performed by the * classes that implement or extend T and pass the filtering test performed by the
* predicate * predicate
*/ */
@SuppressWarnings("unchecked") // we checked the type of each use so we know the casts are safe @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) { Predicate<Class<? extends T>> classFilter) {
if (!hasSearched) {
return List.of();
}
if (isSearching) { if (isSearching) {
throw new IllegalStateException( throw new IllegalStateException(
"Cannot call the getClasses() while the ClassSearcher is searching!"); "Cannot call the getClasses() while the ClassSearcher is searching!");
} }
List<Class<? extends T>> list = new ArrayList<>(); String suffix = getExtensionPointSuffix(ancestorClass.getName());
if (extensionPoints == null) { if (suffix == null) {
return list; return List.of();
} }
for (Class<?> extensionPoint : extensionPoints) { List<Class<? extends T>> list = new ArrayList<>();
if (c.isAssignableFrom(extensionPoint) && for (ClassFileInfo info : extensionPointSuffixToInfoMap.get(suffix)) {
(classFilter == null || classFilter.test((Class<T>) extensionPoint))) {
list.add((Class<? extends T>) extensionPoint); 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; 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) { 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 * Get {@link ExtensionPointProperties#priority() priority-sorted} classes instances that
* instances that implement or derive from the given class * implement or derive from the given filter class and pass the given filter predicate
* *
* @param c the filter class * @param c the filter class
* @param filter A Predicate that tests class objects (that are already of type T) * @param filter A filter predicate that tests class objects (that are already of type T).
* for further filtering, <code>null</code> is equivalent to "return true" * {@code null} is equivalent to "return true".
* @return {@link ExtensionPointProperties#priority() priority-sorted} list of * @return {@link ExtensionPointProperties#priority() priority-sorted} {@link List} of class
* classes instances that implement or extend T and pass the filtering test performed by * instances that implement or extend T and pass the filtering test performed by the predicate
* the predicate
*/ */
public static <T> List<T> getInstances(Class<T> c, ClassFilter filter) { public static <T> List<T> getInstances(Class<T> c, ClassFilter filter) {
List<Class<? extends T>> classes = getClasses(c); 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 * @return A {@link Set} of class information about each discovered potential extension point
* @param monitor the task monitor
* @throws CancelledException if cancelled
* @deprecated use {@link #search(TaskMonitor)} instead
*/ */
@Deprecated(forRemoval = true, since = "10.1") // remove 2 releases after 10.1 public static Set<ClassFileInfo> getExtensionPointInfo() {
public static void search(boolean forceRefresh, TaskMonitor monitor) return extensionPointSuffixToInfoMap.values()
throws CancelledException { .stream()
search(monitor); .flatMap(e -> e.stream())
.collect(Collectors.toSet());
} }
/** /**
* Searches the classpath and updates the list of available classes which * Gets class information about each loaded extension point.
* satisfy the class filter. Classes which * <p>
* data types, and language providers. When the search completes and was * NOTE: Ghidra may load more classes as it runs. Therefore, repeated calls to this method may
* not cancelled, the change listeners are notified. * return more results, as more extension points are loaded.
* *
* @param monitor the progress monitor for the search. * @return A {@link Set} of class information about each loaded extension point
* @throws CancelledException if the operation is cancelled
*/ */
public static void search(TaskMonitor monitor) throws CancelledException { public static Set<ClassFileInfo> getLoaded() {
return loadedCache.keySet();
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);
} }
/** /**
* 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 * @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) { public static String getExtensionPointSuffix(String className) {
if (className.indexOf("Test$") > 0 || className.endsWith("Test")) { if (extensionPointSuffixPattern == null) {
extensionPointSuffixPattern = loadExtensionPointSuffixes();
}
if (className.contains("$") || className.endsWith("Test")) {
return null; return null;
} }
int packageIndex = className.lastIndexOf('.'); int packageIndex = className.lastIndexOf('.');
int innerClassIndex = className.lastIndexOf('$'); if (packageIndex > 0) {
int maximumIndex = StrictMath.max(packageIndex, innerClassIndex); className = className.substring(packageIndex + 1);
if (maximumIndex > 0) {
className = className.substring(maximumIndex + 1);
} }
Matcher m = extensionPointSuffixPattern.matcher(className); Matcher m = extensionPointSuffixPattern.matcher(className);
return m.find() && m.groupCount() == 1 ? m.group(1) : null; 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() { private static List<String> gatherSearchPaths() {
// //
@ -315,7 +492,7 @@ public class ClassSearcher {
private static void getPropertyPaths(String property, List<String> results) { private static void getPropertyPaths(String property, List<String> results) {
String paths = System.getProperty(property); String paths = System.getProperty(property);
Msg.trace(ClassSearcher.class, "Paths in " + property + ": " + paths); log.trace("Paths in {}: {}", property, paths);
if (StringUtils.isBlank(paths)) { if (StringUtils.isBlank(paths)) {
return; return;
} }
@ -351,36 +528,35 @@ public class ClassSearcher {
catch (InvalidPathException e) { catch (InvalidPathException e) {
// we have seen odd strings being placed into the classpath--ignore them, as we // we have seen odd strings being placed into the classpath--ignore them, as we
// don't know how to use them // don't know how to use them
Msg.trace(ClassSearcher.class, "Invalid path '" + path + "'", e); log.trace("Invalid path '{}'", path);
return path; return path;
} }
} }
private static void loadExtensionClassesFromJar() { private static Map<String, Set<ClassFileInfo>> loadExtensionClassesFromJar() {
ResourceFile appRoot = Application.getApplicationRootDirectory(); ResourceFile appRoot = Application.getApplicationRootDirectory();
ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES"); ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES");
try { try {
List<String> classNames = FileUtilities.getLines(extensionClassesFile); List<String> classNames = FileUtilities.getLines(extensionClassesFile);
List<Class<?>> extensionClasses = new ArrayList<>(); Set<ClassFileInfo> extensionClasses = new HashSet<>();
for (String className : classNames) { for (String className : classNames) {
try { String epName = getExtensionPointSuffix(className);
Class<?> clazz = Class.forName(className); if (epName != null) {
extensionClasses.add(clazz); extensionClasses
} .add(new ClassFileInfo(appRoot.getAbsolutePath(), className, epName));
catch (ClassNotFoundException e) {
Msg.warn(ClassSearcher.class, "Can't load extension point: " + className);
} }
} }
extensionPoints = Collections.unmodifiableList(extensionClasses); return extensionClasses.stream()
.collect(Collectors.groupingBy(ClassFileInfo::suffix, Collectors.toSet()));
} }
catch (IOException e) { catch (IOException e) {
throw new AssertException("Unexpected IOException reading extension class file " + throw new AssertException(
extensionClassesFile, e); "Unexpected IOException reading extension class file " + extensionClassesFile, e);
} }
} }
private static void loadExtensionPointSuffixes() { private static Pattern loadExtensionPointSuffixes() {
Set<String> extensionPointSuffixes = new HashSet<>(); Set<String> extensionPointSuffixes = new HashSet<>();
Collection<ResourceFile> moduleRootDirectories = Application.getModuleRootDirectories(); 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(".*("); StringBuilder buffy = new StringBuilder(".*(");
String between = ""; String between = "";
for (String suffix : extensionPointSuffixes) { for (String suffix : extensionPointSuffixes) {
@ -410,12 +606,8 @@ public class ClassSearcher {
between = "|"; between = "|";
} }
buffy.append(")$"); buffy.append(")$");
extensionPointSuffixPattern = Pattern.compile(buffy.toString()); log.trace("Using extension point pattern: {}", buffy);
log.trace("Using extension point pattern: {}", extensionPointSuffixPattern); return Pattern.compile(buffy.toString());
}
static boolean isExtensionPointName(String name) {
return getExtensionPointName(name) != null;
} }
private static void fireClassListChanged() { 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;
}
} }