mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-06 03:50:02 +02:00
Merge remote-tracking branch 'origin/GP-4515_ryanmkurtz_classsearcher--SQUASHED'
This commit is contained in:
commit
6132ddc3d0
16 changed files with 546 additions and 487 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==================================================================================================
|
//==================================================================================================
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue