mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-10-23 05:31:02 +00:00
Merge remote-tracking branch 'origin/GP-4515_ryanmkurtz_classsearcher--SQUASHED'
This commit is contained in:
commit
6132ddc3d0
|
@ -47,6 +47,7 @@ public interface AutoMapSpec extends ExtensionPoint {
|
|||
|
||||
private Private() {
|
||||
ClassSearcher.addChangeListener(classListener);
|
||||
classesChanged(null);
|
||||
}
|
||||
|
||||
private synchronized void classesChanged(ChangeEvent evt) {
|
||||
|
|
|
@ -45,6 +45,7 @@ public interface AutoReadMemorySpec extends ExtensionPoint {
|
|||
|
||||
private Private() {
|
||||
ClassSearcher.addChangeListener(classListener);
|
||||
classesChanged(null);
|
||||
}
|
||||
|
||||
private synchronized void classesChanged(ChangeEvent evt) {
|
||||
|
|
|
@ -89,7 +89,7 @@ public class GhidraRun implements GhidraLaunchable {
|
|||
String projectPath = processArguments(args);
|
||||
openProject(projectPath);
|
||||
|
||||
log.info("Ghidra starup complete (" + GhidraLauncher.getMillisecondsFromLaunch() +
|
||||
log.info("Ghidra startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() +
|
||||
" ms)");
|
||||
});
|
||||
};
|
||||
|
|
|
@ -27,8 +27,7 @@ import generic.jar.ResourceFile;
|
|||
import ghidra.GhidraClassLoader;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Disposable;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.classfinder.ExtensionPoint;
|
||||
import ghidra.util.classfinder.*;
|
||||
|
||||
/**
|
||||
* A dialog that shows useful runtime information
|
||||
|
@ -188,13 +187,32 @@ class RuntimeInfoProvider extends ReusableDialogComponentProvider {
|
|||
* loaded.
|
||||
*/
|
||||
private void addExtensionPoints() {
|
||||
Map<String, String> map = ClassSearcher.getClasses(ExtensionPoint.class)
|
||||
JTabbedPane epTabbedPane = new JTabbedPane();
|
||||
tabbedPane.add("Extension Points", epTabbedPane);
|
||||
|
||||
// Discovered Potential Extension Points
|
||||
Map<String, String> map = ClassSearcher.getExtensionPointInfo()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(e -> e.getName(),
|
||||
e -> ClassSearcher.getExtensionPointName(e.getName())));
|
||||
String name = "Extension Points";
|
||||
tabbedPane.add(new MapTablePanel<String, String>(name, map, "Name", "Extension Point", 400,
|
||||
true, plugin), name);
|
||||
.collect(Collectors.toMap(ClassFileInfo::name, ClassFileInfo::path));
|
||||
String name = "Extension Point Info (%d)".formatted(map.size());
|
||||
epTabbedPane.add(
|
||||
new MapTablePanel<String, String>(name, map, "Name", "Path", 400, true, plugin), name);
|
||||
|
||||
// Loaded Extension Points
|
||||
map = ClassSearcher.getLoaded()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ClassFileInfo::name, ClassFileInfo::suffix));
|
||||
name = "Loaded (%d)".formatted(map.size());
|
||||
epTabbedPane.add(
|
||||
new MapTablePanel<String, String>(name, map, "Name", "Type", 400, true, plugin), name);
|
||||
|
||||
// False Positive Extension Points
|
||||
map = ClassSearcher.getFalsePositives()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ClassFileInfo::name, ClassFileInfo::suffix));
|
||||
name = "False Positives (%d)".formatted(map.size());
|
||||
epTabbedPane.add(
|
||||
new MapTablePanel<String, String>(name, map, "Name", "Type", 400, true, plugin), name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,6 +28,7 @@ import ghidra.framework.*;
|
|||
import ghidra.framework.model.DomainFolder;
|
||||
import ghidra.framework.protocol.ghidra.Handler;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.exception.InvalidInputException;
|
||||
|
||||
/**
|
||||
|
@ -119,6 +120,7 @@ public class AnalyzeHeadless implements GhidraLaunchable {
|
|||
|
||||
Msg.info(AnalyzeHeadless.class,
|
||||
"Headless startup complete (" + GhidraLauncher.getMillisecondsFromLaunch() + " ms)");
|
||||
ClassSearcher.logStatistics();
|
||||
|
||||
// Do the headless processing
|
||||
try {
|
||||
|
|
|
@ -82,9 +82,8 @@ public class FieldNavigator implements ButtonPressedListener, FieldMouseHandlerS
|
|||
new HashMap<Class<?>, List<FieldMouseHandler>>();
|
||||
|
||||
// find all instances of AnnotatedString
|
||||
List<FieldMouseHandlerExtension> instances =
|
||||
ClassSearcher.getInstances(FieldMouseHandlerExtension.class);
|
||||
for (FieldMouseHandlerExtension fieldMouseHandler : instances) {
|
||||
List<FieldMouseHandler> instances = ClassSearcher.getInstances(FieldMouseHandler.class);
|
||||
for (FieldMouseHandler fieldMouseHandler : instances) {
|
||||
addHandler(map, fieldMouseHandler);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.junit.BeforeClass;
|
||||
|
||||
import docking.test.AbstractDockingTest;
|
||||
|
@ -45,6 +46,7 @@ import ghidra.program.model.symbol.Namespace;
|
|||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.util.*;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.classfinder.ClassFileInfo;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.exception.RollbackException;
|
||||
import junit.framework.AssertionFailedError;
|
||||
|
@ -608,29 +610,27 @@ public abstract class AbstractGhidraHeadlessIntegrationTest extends AbstractDock
|
|||
|
||||
ServiceManager serviceManager = (ServiceManager) getInstanceField("serviceMgr", tool);
|
||||
|
||||
List<Class<?>> extentions =
|
||||
(List<Class<?>>) getInstanceField("extensionPoints", ClassSearcher.class);
|
||||
Set<Class<?>> set = new HashSet<>(extentions);
|
||||
Iterator<Class<?>> iterator = set.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Class<?> c = iterator.next();
|
||||
if (service.isAssignableFrom(c)) {
|
||||
iterator.remove();
|
||||
T instance = tool.getService(service);
|
||||
serviceManager.removeService(service, instance);
|
||||
}
|
||||
Map<String, Set<ClassFileInfo>> extensionPointSuffixToInfoMap =
|
||||
(Map<String, Set<ClassFileInfo>>) getInstanceField("extensionPointSuffixToInfoMap",
|
||||
ClassSearcher.class);
|
||||
BidiMap<ClassFileInfo, Class<?>> loadedCache =
|
||||
(BidiMap<ClassFileInfo, Class<?>>) getInstanceField("loadedCache", ClassSearcher.class);
|
||||
String suffix = ClassSearcher.getExtensionPointSuffix(service.getSimpleName());
|
||||
|
||||
if (suffix != null) {
|
||||
Set<ClassFileInfo> serviceSet = extensionPointSuffixToInfoMap.get(suffix);
|
||||
assertNotNull(serviceSet);
|
||||
serviceSet.clear();
|
||||
ClassFileInfo info = new ClassFileInfo("", replacement.getClass().getName(), suffix);
|
||||
serviceSet.add(info);
|
||||
loadedCache.put(info, replacement.getClass());
|
||||
}
|
||||
|
||||
T instance = tool.getService(service);
|
||||
if (instance != null) {
|
||||
serviceManager.removeService(service, instance);
|
||||
}
|
||||
|
||||
set.add(replacement.getClass());
|
||||
serviceManager.addService(service, replacement);
|
||||
|
||||
List<Class<?>> newExtensionPoints = new ArrayList<>(set);
|
||||
setInstanceField("extensionPoints", ClassSearcher.class, newExtensionPoints);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
|
|
|
@ -26,7 +26,6 @@ import generic.jar.*;
|
|||
import ghidra.GhidraApplicationLayout;
|
||||
import ghidra.GhidraLaunchable;
|
||||
import ghidra.framework.*;
|
||||
import ghidra.util.classfinder.ClassFinder;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
|
@ -617,7 +616,7 @@ public class GhidraJarBuilder implements GhidraLaunchable {
|
|||
if (clazz == null) {
|
||||
System.out.println("Couldn't load " + path);
|
||||
}
|
||||
else if (ClassFinder.isClassOfInterest(clazz)) {
|
||||
else if (ClassSearcher.isClassOfInterest(clazz)) {
|
||||
extensionPointClasses.add(clazz.getName());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@ import static org.junit.Assert.*;
|
|||
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.table.TableModel;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.junit.*;
|
||||
|
||||
import docking.ActionContext;
|
||||
|
@ -44,6 +44,7 @@ import ghidra.program.model.listing.Program;
|
|||
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
|
||||
import ghidra.test.TestEnv;
|
||||
import ghidra.util.ColorUtils;
|
||||
import ghidra.util.classfinder.ClassFileInfo;
|
||||
import ghidra.util.classfinder.ClassSearcher;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
|
@ -99,7 +100,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
// The old option's default value should not be applied to the new option
|
||||
//
|
||||
|
||||
installAnalyzer(NotReplacingTestAnalyzerStub.class);
|
||||
installAnalyzer(NotReplacingTestStubAnalyzer.class);
|
||||
|
||||
// install old options; the default value will not be used in the new option
|
||||
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
|
||||
|
@ -122,7 +123,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
// The old option's default value should not be applied to the new option
|
||||
//
|
||||
|
||||
installAnalyzer(NotReplacingTestAnalyzerStub.class);
|
||||
installAnalyzer(NotReplacingTestStubAnalyzer.class);
|
||||
|
||||
// install old options; the default value will not be used in the new option
|
||||
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
|
||||
|
@ -149,7 +150,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
// The old option's non-default value should be applied to the new option
|
||||
//
|
||||
|
||||
installAnalyzer(UseOldValueTestAnalyzerStub.class);
|
||||
installAnalyzer(UseOldValueTestStubAnalyzer.class);
|
||||
|
||||
// install old options; the default value will not be used in the new option
|
||||
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
|
||||
|
@ -175,7 +176,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
// new option has a non-default
|
||||
//
|
||||
|
||||
installAnalyzer(UseOldValueTestAnalyzerStub.class);
|
||||
installAnalyzer(UseOldValueTestStubAnalyzer.class);
|
||||
|
||||
// install old options; the default value will not be used in the new option
|
||||
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
|
||||
|
@ -205,7 +206,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
// has a different object type than the old option.
|
||||
//
|
||||
|
||||
installAnalyzer(ConvertValueTypeTestAnalyzerStub.class);
|
||||
installAnalyzer(ConvertValueTypeTestStubAnalyzer.class);
|
||||
|
||||
// install old options; the default value will not be used in the new option
|
||||
installOldOptions(OLD_OPTION_DEFAULT_VALUE);
|
||||
|
@ -233,12 +234,12 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
|
||||
private void installOldOptions(Object value) {
|
||||
Options programAnalysisOptions = program.getOptions(Program.ANALYSIS_PROPERTIES);
|
||||
Options options = programAnalysisOptions.getOptions(AbstractTestAnalyzerStub.NAME);
|
||||
Options options = programAnalysisOptions.getOptions(AbstractTestStubAnalyzer.NAME);
|
||||
AbstractOptions abstractOptions = (AbstractOptions) getInstanceField("options", options);
|
||||
|
||||
// this call creates an 'unregistered option'
|
||||
String fullOptionName =
|
||||
"Analyzers." + AbstractTestAnalyzerStub.NAME + '.' + OLD_OPTION_NAME;
|
||||
"Analyzers." + AbstractTestStubAnalyzer.NAME + '.' + OLD_OPTION_NAME;
|
||||
Option option = abstractOptions.getOption(fullOptionName, OptionType.getOptionType(value),
|
||||
OLD_OPTION_DEFAULT_VALUE);
|
||||
|
||||
|
@ -249,16 +250,23 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void installAnalyzer(Class<? extends Analyzer> analyzer) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Class<?>> extensions =
|
||||
(List<Class<?>>) getInstanceField("extensionPoints", ClassSearcher.class);
|
||||
Map<String, Set<ClassFileInfo>> extensionPointSuffixToInfoMap =
|
||||
(Map<String, Set<ClassFileInfo>>) getInstanceField("extensionPointSuffixToInfoMap",
|
||||
ClassSearcher.class);
|
||||
BidiMap<ClassFileInfo, Class<?>> loadedCache =
|
||||
(BidiMap<ClassFileInfo, Class<?>>) getInstanceField("loadedCache", ClassSearcher.class);
|
||||
|
||||
// remove any traces of previous test runs
|
||||
extensions.removeIf(c -> c.getSimpleName().contains("TestAnalyzerStub"));
|
||||
Set<ClassFileInfo> analyzerSet = extensionPointSuffixToInfoMap.get("Analyzer");
|
||||
assertNotNull(analyzerSet);
|
||||
|
||||
extensions.add(analyzer);
|
||||
analyzerSet.removeIf(c -> c.name().contains("TestStubAnalyzer"));
|
||||
ClassFileInfo info = new ClassFileInfo("", analyzer.getName(), "Analyzer");
|
||||
analyzerSet.add(info);
|
||||
loadedCache.put(info, analyzer);
|
||||
}
|
||||
|
||||
private AnalysisOptionsDialog invokeAnalysisDialog() {
|
||||
|
@ -284,19 +292,19 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
|
||||
private void changeNewOption(String newValue) {
|
||||
Options options = program.getOptions(Program.ANALYSIS_PROPERTIES);
|
||||
Options analyzerOptions = options.getOptions(AbstractTestAnalyzerStub.NAME);
|
||||
Options analyzerOptions = options.getOptions(AbstractTestStubAnalyzer.NAME);
|
||||
|
||||
tx(program, () -> analyzerOptions.putObject(NEW_OPTION_NAME, newValue));
|
||||
}
|
||||
|
||||
private void changeOldOption(String newValue) {
|
||||
Options programAnalysisOptions = program.getOptions(Program.ANALYSIS_PROPERTIES);
|
||||
Options options = programAnalysisOptions.getOptions(AbstractTestAnalyzerStub.NAME);
|
||||
Options options = programAnalysisOptions.getOptions(AbstractTestStubAnalyzer.NAME);
|
||||
AbstractOptions abstractOptions = (AbstractOptions) getInstanceField("options", options);
|
||||
|
||||
// this call creates an 'unregistered option'
|
||||
String fullOptionName =
|
||||
"Analyzers." + AbstractTestAnalyzerStub.NAME + '.' + OLD_OPTION_NAME;
|
||||
"Analyzers." + AbstractTestStubAnalyzer.NAME + '.' + OLD_OPTION_NAME;
|
||||
Option option = abstractOptions.getOption(fullOptionName,
|
||||
OptionType.getOptionType(OLD_OPTION_DEFAULT_VALUE), OLD_OPTION_DEFAULT_VALUE);
|
||||
|
||||
|
@ -309,14 +317,14 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
|
||||
private void assertOldValueRemoved() {
|
||||
Options options = program.getOptions(Program.ANALYSIS_PROPERTIES);
|
||||
Options analyzerOptions = options.getOptions(AbstractTestAnalyzerStub.NAME);
|
||||
Options analyzerOptions = options.getOptions(AbstractTestStubAnalyzer.NAME);
|
||||
assertFalse("Old option not removed", analyzerOptions.contains(OLD_OPTION_NAME));
|
||||
}
|
||||
|
||||
private void assertOnlyNewOptionsInUi() {
|
||||
|
||||
// click our analyzer in the list of options
|
||||
selectAnalyzer(AbstractTestAnalyzerStub.NAME);
|
||||
selectAnalyzer(AbstractTestStubAnalyzer.NAME);
|
||||
|
||||
// get the panel of options
|
||||
JPanel panel =
|
||||
|
@ -340,7 +348,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
|
||||
private void assertOptionValue(String optionName, Object defaultValue) {
|
||||
Options options = program.getOptions(Program.ANALYSIS_PROPERTIES);
|
||||
Options analyzerOptions = options.getOptions(AbstractTestAnalyzerStub.NAME);
|
||||
Options analyzerOptions = options.getOptions(AbstractTestStubAnalyzer.NAME);
|
||||
Object value = analyzerOptions.getObject(optionName, null);
|
||||
assertEquals("Option value is not as expected for '" + optionName + "'", defaultValue,
|
||||
value);
|
||||
|
@ -387,7 +395,7 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
public static abstract class AbstractTestAnalyzerStub implements Analyzer {
|
||||
public static abstract class AbstractTestStubAnalyzer implements Analyzer {
|
||||
|
||||
protected AnalysisOptionsUpdater updater = new AnalysisOptionsUpdater();
|
||||
|
||||
|
@ -472,9 +480,9 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
public static class NotReplacingTestAnalyzerStub extends AbstractTestAnalyzerStub {
|
||||
public static class NotReplacingTestStubAnalyzer extends AbstractTestStubAnalyzer {
|
||||
|
||||
public NotReplacingTestAnalyzerStub() {
|
||||
public NotReplacingTestStubAnalyzer() {
|
||||
super();
|
||||
|
||||
updater.registerReplacement(NEW_OPTION_NAME, OLD_OPTION_NAME, oldValue -> {
|
||||
|
@ -485,9 +493,9 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
public static class UseOldValueTestAnalyzerStub extends AbstractTestAnalyzerStub {
|
||||
public static class UseOldValueTestStubAnalyzer extends AbstractTestStubAnalyzer {
|
||||
|
||||
public UseOldValueTestAnalyzerStub() {
|
||||
public UseOldValueTestStubAnalyzer() {
|
||||
super();
|
||||
|
||||
updater.registerReplacement(NEW_OPTION_NAME, OLD_OPTION_NAME, oldValue -> {
|
||||
|
@ -496,9 +504,9 @@ public class AnalysisOptions2Test extends AbstractGhidraHeadedIntegrationTest {
|
|||
}
|
||||
}
|
||||
|
||||
public static class ConvertValueTypeTestAnalyzerStub extends AbstractTestAnalyzerStub {
|
||||
public static class ConvertValueTypeTestStubAnalyzer extends AbstractTestStubAnalyzer {
|
||||
|
||||
public ConvertValueTypeTestAnalyzerStub() {
|
||||
public ConvertValueTypeTestStubAnalyzer() {
|
||||
super();
|
||||
|
||||
updater.registerReplacement(NEW_OPTION_NAME, OLD_OPTION_NAME, oldValue -> {
|
||||
|
|
|
@ -33,7 +33,7 @@ class ClassDir {
|
|||
classPackage = new ClassPackage(dir, "", monitor);
|
||||
}
|
||||
|
||||
void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException {
|
||||
void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) throws CancelledException {
|
||||
classPackage.getClasses(set, monitor);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 utility.application.ApplicationLayout;
|
||||
|
||||
class ClassJar extends ClassLocation {
|
||||
class ClassJar implements ClassLocation {
|
||||
|
||||
/**
|
||||
* Pattern for matching jar files in a module lib dir
|
||||
|
@ -51,6 +51,7 @@ class ClassJar extends ClassLocation {
|
|||
private static final String PATCH_DIR_PATH_FORWARD_SLASHED = getPatchDirPath();
|
||||
private static final Set<String> USER_PLUGIN_PATHS = loadUserPluginPaths();
|
||||
|
||||
private Set<ClassFileInfo> classes = new HashSet<>();
|
||||
private String path;
|
||||
|
||||
ClassJar(String path, TaskMonitor monitor) throws CancelledException {
|
||||
|
@ -61,8 +62,7 @@ class ClassJar extends ClassLocation {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void getClasses(Set<Class<?>> set, TaskMonitor monitor) {
|
||||
checkForDuplicates(set);
|
||||
public void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) {
|
||||
set.addAll(classes);
|
||||
}
|
||||
|
||||
|
@ -174,9 +174,9 @@ class ClassJar extends ClassLocation {
|
|||
name = name.substring(0, name.indexOf(CLASS_EXT));
|
||||
name = name.replace('/', '.');
|
||||
|
||||
Class<?> c = ClassFinder.loadExtensionPoint(path, name);
|
||||
if (c != null) {
|
||||
classes.add(c);
|
||||
String epName = ClassSearcher.getExtensionPointSuffix(name);
|
||||
if (epName != null) {
|
||||
classes.add(new ClassFileInfo(path, name, epName));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,52 +15,17 @@
|
|||
*/
|
||||
package ghidra.util.classfinder;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* Represents a place from which {@link Class}s can be obtained
|
||||
*/
|
||||
abstract class ClassLocation {
|
||||
interface ClassLocation {
|
||||
|
||||
protected static final String CLASS_EXT = ".class";
|
||||
public static final String CLASS_EXT = ".class";
|
||||
|
||||
protected final Logger log = LogManager.getLogger(getClass());
|
||||
|
||||
protected Set<Class<?>> classes = new HashSet<>();
|
||||
|
||||
protected abstract void getClasses(Set<Class<?>> set, TaskMonitor monitor)
|
||||
throws CancelledException;
|
||||
|
||||
protected void checkForDuplicates(Set<Class<?>> existingClasses) {
|
||||
for (Class<?> c : classes) {
|
||||
// Note: our class and a matching class in 'existingClasses' will be '==' since the
|
||||
// class loader loaded the class by name--it will always find the same class, in
|
||||
// classpath order.
|
||||
if (existingClasses.contains(c)) {
|
||||
log.warn(() -> generateMessage(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String generateMessage(Class<?> c) {
|
||||
return String.format("Class defined in multiple locations: %s. Keeping class loaded " +
|
||||
"from %s; ignoring class from %s", c, toLocation(c), this);
|
||||
}
|
||||
|
||||
private String toLocation(Class<?> clazz) {
|
||||
String name = clazz.getName();
|
||||
String classAsPath = '/' + name.replace('.', '/') + ".class";
|
||||
URL url = clazz.getResource(classAsPath);
|
||||
String urlPath = url.getPath();
|
||||
int index = urlPath.indexOf(classAsPath);
|
||||
return urlPath.substring(0, index);
|
||||
}
|
||||
public void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) throws CancelledException;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ import ghidra.util.Msg;
|
|||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
class ClassPackage extends ClassLocation {
|
||||
class ClassPackage implements ClassLocation {
|
||||
|
||||
private static final FileFilter CLASS_FILTER =
|
||||
pathname -> pathname.getName().endsWith(CLASS_EXT);
|
||||
|
@ -32,6 +32,7 @@ class ClassPackage extends ClassLocation {
|
|||
private File rootDir;
|
||||
private File packageDir;
|
||||
private String packageName;
|
||||
private Set<ClassFileInfo> classes = new HashSet<>();
|
||||
|
||||
ClassPackage(File rootDir, String packageName, TaskMonitor monitor) throws CancelledException {
|
||||
monitor.checkCancelled();
|
||||
|
@ -47,9 +48,9 @@ class ClassPackage extends ClassLocation {
|
|||
String path = rootDir.getAbsolutePath();
|
||||
Set<String> allClassNames = getAllClassNames();
|
||||
for (String className : allClassNames) {
|
||||
Class<?> c = ClassFinder.loadExtensionPoint(path, className);
|
||||
if (c != null) {
|
||||
classes.add(c);
|
||||
String epName = ClassSearcher.getExtensionPointSuffix(className);
|
||||
if (epName != null) {
|
||||
classes.add(new ClassFileInfo(path, className, epName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,9 +89,7 @@ class ClassPackage extends ClassLocation {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException {
|
||||
|
||||
checkForDuplicates(set);
|
||||
public void getClasses(Set<ClassFileInfo> set, TaskMonitor monitor) throws CancelledException {
|
||||
|
||||
set.addAll(classes);
|
||||
|
||||
|
|
|
@ -18,7 +18,10 @@ package ghidra.util.classfinder;
|
|||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.nio.file.*;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
|
@ -27,21 +30,25 @@ import java.util.stream.Collectors;
|
|||
|
||||
import javax.swing.event.ChangeListener;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import generic.jar.ResourceFile;
|
||||
import generic.json.Json;
|
||||
import ghidra.GhidraClassLoader;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.*;
|
||||
import ghidra.util.datastruct.WeakDataStructureFactory;
|
||||
import ghidra.util.datastruct.WeakSet;
|
||||
import ghidra.util.exception.AssertException;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.extensions.*;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import utilities.util.FileUtilities;
|
||||
import utility.module.ModuleUtilities;
|
||||
|
||||
/**
|
||||
* This class is a collection of static methods used to discover classes that implement a
|
||||
|
@ -60,88 +67,171 @@ import utilities.util.FileUtilities;
|
|||
*/
|
||||
public class ClassSearcher {
|
||||
|
||||
// This provides a means for custom apps that do not use a module structure to search all jars
|
||||
private static final Logger log = LogManager.getLogger(ClassSearcher.class);
|
||||
|
||||
/**
|
||||
* This provides a means for custom apps that do not use a module structure to search all jars
|
||||
*/
|
||||
public static final String SEARCH_ALL_JARS_PROPERTY = "class.searcher.search.all.jars";
|
||||
private static final String SEARCH_ALL_JARS_PROPERTY_VALUE =
|
||||
System.getProperty(SEARCH_ALL_JARS_PROPERTY, Boolean.FALSE.toString());
|
||||
static final boolean SEARCH_ALL_JARS = Boolean.parseBoolean(SEARCH_ALL_JARS_PROPERTY_VALUE);
|
||||
static final boolean SEARCH_ALL_JARS = Boolean.getBoolean(SEARCH_ALL_JARS_PROPERTY);
|
||||
|
||||
static final Logger log = LogManager.getLogger(ClassSearcher.class);
|
||||
|
||||
private static ClassFinder searcher;
|
||||
private static List<Class<?>> extensionPoints;
|
||||
private static final boolean IS_USING_RESTRICTED_EXTENSIONS =
|
||||
Boolean.getBoolean(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY);
|
||||
|
||||
private static List<Class<?>> FILTER_CLASSES = Arrays.asList(ExtensionPoint.class);
|
||||
private static Pattern extensionPointSuffixPattern;
|
||||
private static Map<String, Set<ClassFileInfo>> extensionPointSuffixToInfoMap;
|
||||
private static BidiMap<ClassFileInfo, Class<?>> loadedCache = new DualHashBidiMap<>();
|
||||
private static Set<ClassFileInfo> falsePositiveCache = new HashSet<>();
|
||||
private static volatile boolean hasSearched;
|
||||
private static volatile boolean isSearching;
|
||||
private static WeakSet<ChangeListener> listenerList =
|
||||
WeakDataStructureFactory.createCopyOnReadWeakSet();
|
||||
|
||||
private static Pattern extensionPointSuffixPattern;
|
||||
|
||||
private static volatile boolean hasSearched;
|
||||
private static volatile boolean isSearching;
|
||||
private static final ClassFilter DO_NOTHING_FILTER = c -> true;
|
||||
|
||||
/**
|
||||
* Prevent class instantiation
|
||||
*/
|
||||
private ClassSearcher() {
|
||||
// you cannot create one of these
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the classpath and updates the list of available classes which satisfies the
|
||||
* internal class filter. When the search completes (and was not cancelled), any registered
|
||||
* change listeners are notified.
|
||||
*
|
||||
* @param monitor the progress monitor for the search
|
||||
* @throws CancelledException if the operation was cancelled
|
||||
*/
|
||||
public static void search(TaskMonitor monitor) throws CancelledException {
|
||||
|
||||
if (hasSearched) {
|
||||
log.trace("Already searched for classes: using cached results");
|
||||
return;
|
||||
}
|
||||
|
||||
log.trace("Using restricted extension class loader? " + IS_USING_RESTRICTED_EXTENSIONS);
|
||||
|
||||
Instant start = Instant.now();
|
||||
isSearching = true;
|
||||
|
||||
if (Application.inSingleJarMode()) {
|
||||
log.trace("Single Jar Mode: using extensions from the jar file");
|
||||
extensionPointSuffixToInfoMap = loadExtensionClassesFromJar();
|
||||
}
|
||||
else {
|
||||
extensionPointSuffixToInfoMap = findClasses(monitor);
|
||||
}
|
||||
|
||||
log.trace("Found extension classes {}", extensionPointSuffixToInfoMap);
|
||||
if (extensionPointSuffixToInfoMap.isEmpty()) {
|
||||
throw new AssertException("Unable to locate extension points!");
|
||||
}
|
||||
|
||||
hasSearched = true;
|
||||
isSearching = false;
|
||||
|
||||
Swing.runNow(() -> fireClassListChanged());
|
||||
|
||||
String finishedMessage =
|
||||
"Class search complete (" + ChronoUnit.MILLIS.between(start, Instant.now()) + " ms)";
|
||||
monitor.setMessage(finishedMessage);
|
||||
log.info(finishedMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes that implement or
|
||||
* derive from the given class
|
||||
* derive from the given ancestor class
|
||||
*
|
||||
* @param c the filter class
|
||||
* @param ancestorClass the ancestor class
|
||||
* @return set of classes that implement or extend T
|
||||
*/
|
||||
public static <T> List<Class<? extends T>> getClasses(Class<T> c) {
|
||||
return getClasses(c, null);
|
||||
public static <T> List<Class<? extends T>> getClasses(Class<T> ancestorClass) {
|
||||
return getClasses(ancestorClass, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes that
|
||||
* implement or derive from the given class
|
||||
* implement or derive from the given ancestor class
|
||||
*
|
||||
* @param c the filter class
|
||||
* @param ancestorClass the ancestor class
|
||||
* @param classFilter A Predicate that tests class objects (that are already of type T)
|
||||
* for further filtering, <code>null</code> is equivalent to "return true"
|
||||
* for further filtering, {@code null} is equivalent to "return true"
|
||||
* @return {@link ExtensionPointProperties#priority() priority-sorted} list of
|
||||
* classes that implement or extend T and pass the filtering test performed by the
|
||||
* predicate
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // we checked the type of each use so we know the casts are safe
|
||||
public static <T> List<Class<? extends T>> getClasses(Class<T> c,
|
||||
public static <T> List<Class<? extends T>> getClasses(Class<T> ancestorClass,
|
||||
Predicate<Class<? extends T>> classFilter) {
|
||||
if (!hasSearched) {
|
||||
return List.of();
|
||||
}
|
||||
if (isSearching) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot call the getClasses() while the ClassSearcher is searching!");
|
||||
}
|
||||
|
||||
List<Class<? extends T>> list = new ArrayList<>();
|
||||
if (extensionPoints == null) {
|
||||
return list;
|
||||
String suffix = getExtensionPointSuffix(ancestorClass.getName());
|
||||
if (suffix == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
for (Class<?> extensionPoint : extensionPoints) {
|
||||
if (c.isAssignableFrom(extensionPoint) &&
|
||||
(classFilter == null || classFilter.test((Class<T>) extensionPoint))) {
|
||||
list.add((Class<? extends T>) extensionPoint);
|
||||
List<Class<? extends T>> list = new ArrayList<>();
|
||||
for (ClassFileInfo info : extensionPointSuffixToInfoMap.get(suffix)) {
|
||||
|
||||
if (falsePositiveCache.contains(info)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<?> c = loadedCache.get(info);
|
||||
if (c == null) {
|
||||
c = loadExtensionPoint(info.path(), info.name());
|
||||
ClassFileInfo existing = loadedCache.getKey(c);
|
||||
if (existing != null) {
|
||||
log.info(
|
||||
"Skipping load of class '%s' from '%s'. Already loaded from '%s'."
|
||||
.formatted(info.name(), info.path(), existing.path()));
|
||||
}
|
||||
if (c == null) {
|
||||
falsePositiveCache.add(info);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
loadedCache.put(info, c);
|
||||
|
||||
if (ancestorClass.isAssignableFrom(c) &&
|
||||
(classFilter == null || classFilter.test((Class<T>) c))) {
|
||||
list.add((Class<? extends T>) c);
|
||||
}
|
||||
}
|
||||
|
||||
prioritizeClasses(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all {@link ExtensionPointProperties#priority() priority-sorted} class instances that
|
||||
* implement or derive from the given filter class
|
||||
*
|
||||
* @param c the filter class
|
||||
* @return {@link ExtensionPointProperties#priority() priority-sorted} {@link List} of
|
||||
* class instances that implement or extend T
|
||||
*/
|
||||
public static <T> List<T> getInstances(Class<T> c) {
|
||||
return getInstances(c, DO_NOTHING_FILTER);
|
||||
return getInstances(c, filter -> true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes
|
||||
* instances that implement or derive from the given class
|
||||
* Get {@link ExtensionPointProperties#priority() priority-sorted} classes instances that
|
||||
* implement or derive from the given filter class and pass the given filter predicate
|
||||
*
|
||||
* @param c the filter class
|
||||
* @param filter A Predicate that tests class objects (that are already of type T)
|
||||
* for further filtering, <code>null</code> is equivalent to "return true"
|
||||
* @return {@link ExtensionPointProperties#priority() priority-sorted} list of
|
||||
* classes instances that implement or extend T and pass the filtering test performed by
|
||||
* the predicate
|
||||
* @param filter A filter predicate that tests class objects (that are already of type T).
|
||||
* {@code null} is equivalent to "return true".
|
||||
* @return {@link ExtensionPointProperties#priority() priority-sorted} {@link List} of class
|
||||
* instances that implement or extend T and pass the filtering test performed by the predicate
|
||||
*/
|
||||
public static <T> List<T> getInstances(Class<T> c, ClassFilter filter) {
|
||||
List<Class<? extends T>> classes = getClasses(c);
|
||||
|
@ -214,91 +304,178 @@ public class ClassSearcher {
|
|||
}
|
||||
|
||||
/**
|
||||
* This deprecated method is now simply a pass-through for {@link #search(TaskMonitor)}.
|
||||
* Gets class information about each discovered potential extension point.
|
||||
* <p>
|
||||
* NOTE: A discovered potential extension point may end up not getting loaded if it is not
|
||||
* "of interest" (see {@link #isClassOfInterest(Class)}. These are referred to as false
|
||||
* positives.
|
||||
*
|
||||
* @param forceRefresh ignored
|
||||
* @param monitor the task monitor
|
||||
* @throws CancelledException if cancelled
|
||||
* @deprecated use {@link #search(TaskMonitor)} instead
|
||||
* @return A {@link Set} of class information about each discovered potential extension point
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "10.1") // remove 2 releases after 10.1
|
||||
public static void search(boolean forceRefresh, TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
search(monitor);
|
||||
public static Set<ClassFileInfo> getExtensionPointInfo() {
|
||||
return extensionPointSuffixToInfoMap.values()
|
||||
.stream()
|
||||
.flatMap(e -> e.stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the classpath and updates the list of available classes which
|
||||
* satisfy the class filter. Classes which
|
||||
* data types, and language providers. When the search completes and was
|
||||
* not cancelled, the change listeners are notified.
|
||||
*
|
||||
* @param monitor the progress monitor for the search.
|
||||
* @throws CancelledException if the operation is cancelled
|
||||
* Gets class information about each loaded extension point.
|
||||
* <p>
|
||||
* NOTE: Ghidra may load more classes as it runs. Therefore, repeated calls to this method may
|
||||
* return more results, as more extension points are loaded.
|
||||
*
|
||||
* @return A {@link Set} of class information about each loaded extension point
|
||||
*/
|
||||
public static void search(TaskMonitor monitor) throws CancelledException {
|
||||
|
||||
if (hasSearched) {
|
||||
log.trace("Already searched for classes: using cached results");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Application.inSingleJarMode()) {
|
||||
log.trace("Single Jar Mode: using extensions from the jar file");
|
||||
loadExtensionClassesFromJar();
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching = true;
|
||||
|
||||
loadExtensionPointSuffixes();
|
||||
|
||||
extensionPoints = null;
|
||||
|
||||
long t = (new Date()).getTime();
|
||||
|
||||
log.info("Searching for classes...");
|
||||
List<String> searchPaths = gatherSearchPaths();
|
||||
searcher = new ClassFinder(searchPaths, monitor);
|
||||
|
||||
monitor.setMessage("Loading classes...");
|
||||
extensionPoints = searcher.getClasses(monitor);
|
||||
log.trace("Found extension classes {}", extensionPoints);
|
||||
if (extensionPoints.isEmpty()) {
|
||||
throw new AssertException("Unable to location extension points!");
|
||||
}
|
||||
|
||||
hasSearched = true;
|
||||
isSearching = false;
|
||||
|
||||
SystemUtilities.runSwingNow(() -> fireClassListChanged());
|
||||
|
||||
String finishedMessage = "Class search complete (%d ms, %d classes loaded)"
|
||||
.formatted((new Date()).getTime() - t, extensionPoints.size());
|
||||
monitor.setMessage(finishedMessage);
|
||||
log.info(finishedMessage);
|
||||
public static Set<ClassFileInfo> getLoaded() {
|
||||
return loadedCache.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given class's extension point name
|
||||
* Gets class information about discovered potential extension points that end up not getting
|
||||
* loaded.
|
||||
* <p>
|
||||
* NOTE: Ghidra may load more classes as it runs. Therefore, repeated calls to this method may
|
||||
* return more results, as more potential extension points are identified as false positives.
|
||||
*
|
||||
* @return A {@link Set} of class information about each loaded extension point
|
||||
*/
|
||||
public static Set<ClassFileInfo> getFalsePositives() {
|
||||
return falsePositiveCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given class's extension point suffix.
|
||||
* <p>
|
||||
* Note that if multiple suffixes match, the smallest will be chosen. For a detailed
|
||||
* explanation, see the comment inside {@link #loadExtensionPointSuffixes()}.
|
||||
*
|
||||
* @param className The name of the potential extension point class
|
||||
* @return The given class's extension point name, or null if it is not an extension point
|
||||
* @return The given class's extension point suffix, or null if it is not an extension point or
|
||||
* {@link #search(TaskMonitor)} has not been called yet
|
||||
*/
|
||||
public static String getExtensionPointName(String className) {
|
||||
if (className.indexOf("Test$") > 0 || className.endsWith("Test")) {
|
||||
public static String getExtensionPointSuffix(String className) {
|
||||
if (extensionPointSuffixPattern == null) {
|
||||
extensionPointSuffixPattern = loadExtensionPointSuffixes();
|
||||
}
|
||||
if (className.contains("$") || className.endsWith("Test")) {
|
||||
return null;
|
||||
}
|
||||
int packageIndex = className.lastIndexOf('.');
|
||||
int innerClassIndex = className.lastIndexOf('$');
|
||||
int maximumIndex = StrictMath.max(packageIndex, innerClassIndex);
|
||||
if (maximumIndex > 0) {
|
||||
className = className.substring(maximumIndex + 1);
|
||||
if (packageIndex > 0) {
|
||||
className = className.substring(packageIndex + 1);
|
||||
}
|
||||
Matcher m = extensionPointSuffixPattern.matcher(className);
|
||||
return m.find() && m.groupCount() == 1 ? m.group(1) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the given class is an extension point of interest.
|
||||
*
|
||||
* @param c The class to check.
|
||||
* @return True if the given class is an extension point of interest; otherwise, false.
|
||||
*/
|
||||
public static boolean isClassOfInterest(Class<?> c) {
|
||||
if (Modifier.isAbstract(c.getModifiers())) { // we don't support abstract (includes interfaces)
|
||||
return false;
|
||||
}
|
||||
if (c.getEnclosingClass() != null) { // we don't support inner classes
|
||||
return false;
|
||||
}
|
||||
if (!Modifier.isPublic(c.getModifiers())) { // we don't support non-public
|
||||
return false;
|
||||
}
|
||||
if (ExtensionPointProperties.Util.isExcluded(c)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Class<?> filterClass : FILTER_CLASSES) {
|
||||
if (filterClass.isAssignableFrom(c)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the current class searcher statistics to the info log
|
||||
*/
|
||||
public static void logStatistics() {
|
||||
log.info("Class searcher loaded %d extension points (%d false positives)"
|
||||
.formatted(loadedCache.size(), falsePositiveCache.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the disk to find potential extension point class files. Matching is performed by file
|
||||
* name only. The class files are not opened or loaded by this method.
|
||||
*
|
||||
* @param monitor the progress monitor for the disk scan
|
||||
* @return A {@link Map} of discovered {@link ClassFileInfo class information}, keyed by their
|
||||
* extension point suffix
|
||||
* @throws CancelledException if the user cancelled the operation
|
||||
*/
|
||||
private static Map<String, Set<ClassFileInfo>> findClasses(TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
log.info("Searching for classes...");
|
||||
|
||||
Set<ClassDir> classDirs = new HashSet<>();
|
||||
Set<ClassJar> classJars = new HashSet<>();
|
||||
|
||||
for (String searchPath : gatherSearchPaths()) {
|
||||
String lcSearchPath = searchPath.toLowerCase();
|
||||
File searchFile = new File(searchPath);
|
||||
if ((lcSearchPath.endsWith(".jar") || lcSearchPath.endsWith(".zip")) &&
|
||||
searchFile.exists()) {
|
||||
|
||||
if (ClassJar.ignoreJar(searchPath)) {
|
||||
log.trace("Ignoring jar file: {}", searchPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.trace("Searching jar file: {}", searchPath);
|
||||
classJars.add(new ClassJar(searchPath, monitor));
|
||||
}
|
||||
else if (searchFile.isDirectory()) {
|
||||
log.trace("Searching classpath directory: {}", searchPath);
|
||||
classDirs.add(new ClassDir(searchPath, monitor));
|
||||
}
|
||||
}
|
||||
|
||||
Set<ClassFileInfo> classSet = new HashSet<>();
|
||||
for (ClassDir dir : classDirs) {
|
||||
monitor.checkCancelled();
|
||||
dir.getClasses(classSet, monitor);
|
||||
}
|
||||
for (ClassJar jar : classJars) {
|
||||
monitor.checkCancelled();
|
||||
jar.getClasses(classSet, monitor);
|
||||
}
|
||||
|
||||
return classSet.stream()
|
||||
.collect(Collectors.groupingBy(ClassFileInfo::suffix, Collectors.toSet()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the given {@link List} of {@link Class}es first by their defined priority, then by
|
||||
* their name.
|
||||
*
|
||||
* @param list The {@link List} of {@link Class}es to sort. This {@link List} will be modified.
|
||||
*/
|
||||
private static <T> void prioritizeClasses(List<Class<? extends T>> list) {
|
||||
Collections.sort(list, (c1, c2) -> {
|
||||
// Sort classes primarily by priority and secondarily by name
|
||||
int p1 = ExtensionPointProperties.Util.getPriority(c1);
|
||||
int p2 = ExtensionPointProperties.Util.getPriority(c2);
|
||||
if (p1 > p2) {
|
||||
return -1;
|
||||
}
|
||||
if (p1 < p2) {
|
||||
return 1;
|
||||
}
|
||||
return c1.getName().compareTo(c2.getName());
|
||||
});
|
||||
}
|
||||
|
||||
private static List<String> gatherSearchPaths() {
|
||||
|
||||
//
|
||||
|
@ -315,7 +492,7 @@ public class ClassSearcher {
|
|||
|
||||
private static void getPropertyPaths(String property, List<String> results) {
|
||||
String paths = System.getProperty(property);
|
||||
Msg.trace(ClassSearcher.class, "Paths in " + property + ": " + paths);
|
||||
log.trace("Paths in {}: {}", property, paths);
|
||||
if (StringUtils.isBlank(paths)) {
|
||||
return;
|
||||
}
|
||||
|
@ -351,36 +528,35 @@ public class ClassSearcher {
|
|||
catch (InvalidPathException e) {
|
||||
// we have seen odd strings being placed into the classpath--ignore them, as we
|
||||
// don't know how to use them
|
||||
Msg.trace(ClassSearcher.class, "Invalid path '" + path + "'", e);
|
||||
log.trace("Invalid path '{}'", path);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadExtensionClassesFromJar() {
|
||||
private static Map<String, Set<ClassFileInfo>> loadExtensionClassesFromJar() {
|
||||
ResourceFile appRoot = Application.getApplicationRootDirectory();
|
||||
ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES");
|
||||
try {
|
||||
List<String> classNames = FileUtilities.getLines(extensionClassesFile);
|
||||
List<Class<?>> extensionClasses = new ArrayList<>();
|
||||
Set<ClassFileInfo> extensionClasses = new HashSet<>();
|
||||
for (String className : classNames) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(className);
|
||||
extensionClasses.add(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
Msg.warn(ClassSearcher.class, "Can't load extension point: " + className);
|
||||
String epName = getExtensionPointSuffix(className);
|
||||
if (epName != null) {
|
||||
extensionClasses
|
||||
.add(new ClassFileInfo(appRoot.getAbsolutePath(), className, epName));
|
||||
}
|
||||
}
|
||||
extensionPoints = Collections.unmodifiableList(extensionClasses);
|
||||
return extensionClasses.stream()
|
||||
.collect(Collectors.groupingBy(ClassFileInfo::suffix, Collectors.toSet()));
|
||||
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new AssertException("Unexpected IOException reading extension class file " +
|
||||
extensionClassesFile, e);
|
||||
throw new AssertException(
|
||||
"Unexpected IOException reading extension class file " + extensionClassesFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void loadExtensionPointSuffixes() {
|
||||
private static Pattern loadExtensionPointSuffixes() {
|
||||
Set<String> extensionPointSuffixes = new HashSet<>();
|
||||
|
||||
Collection<ResourceFile> moduleRootDirectories = Application.getModuleRootDirectories();
|
||||
|
@ -397,6 +573,26 @@ public class ClassSearcher {
|
|||
}
|
||||
}
|
||||
|
||||
// Build regex of the form .*(suffix1|suffix2|suffix3|...)$
|
||||
// If one suffix ends with another suffix, precedence should be given to the shorter one.
|
||||
// This will result in some false positives, but will prevent some corner error cases as
|
||||
// described in this example:
|
||||
// There are 2 valid suffixes, Plugin and BobPlugin. Someone makes a new class:
|
||||
//
|
||||
// class BillBobPlugin extends Plugin
|
||||
//
|
||||
// The person who made this class was unaware that BobPlugin was also a valid suffix. If
|
||||
// we were to match on the longest suffix, BillBobPlugin would erroneously be associated
|
||||
// with BobPlugin, and getClasses() would fail. Now consider this example:
|
||||
//
|
||||
// class BillBobPlugin extends BobPlugin
|
||||
//
|
||||
// Since BillBobPlugin will be associated with the shorter "Plugin" suffix, it will be
|
||||
// grouped with the other Plugin extension points. However, when getClasses(BobPlugin.class)
|
||||
// is called, we retrieve the same shortest suffix from the given "BobPlugin" class name,
|
||||
// which is Plugin. This will result in BillBobPlugin getting properly discovered from the
|
||||
// Plugin group. Final checks are performed to make sure the provided class is assignable
|
||||
// from any class in the group, which filters out the bad associations.
|
||||
StringBuilder buffy = new StringBuilder(".*(");
|
||||
String between = "";
|
||||
for (String suffix : extensionPointSuffixes) {
|
||||
|
@ -410,12 +606,8 @@ public class ClassSearcher {
|
|||
between = "|";
|
||||
}
|
||||
buffy.append(")$");
|
||||
extensionPointSuffixPattern = Pattern.compile(buffy.toString());
|
||||
log.trace("Using extension point pattern: {}", extensionPointSuffixPattern);
|
||||
}
|
||||
|
||||
static boolean isExtensionPointName(String name) {
|
||||
return getExtensionPointName(name) != null;
|
||||
log.trace("Using extension point pattern: {}", buffy);
|
||||
return Pattern.compile(buffy.toString());
|
||||
}
|
||||
|
||||
private static void fireClassListChanged() {
|
||||
|
@ -429,4 +621,107 @@ public class ClassSearcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given class name matches the known extension name patterns, then this method will try
|
||||
* to load that class using the provided path. Extensions may be loaded using their own
|
||||
* class loader, depending on the system property
|
||||
* {@link GhidraClassLoader#ENABLE_RESTRICTED_EXTENSIONS_PROPERTY}.
|
||||
* <p>
|
||||
* Examples:
|
||||
* <pre>
|
||||
* /foo/bar/baz/file.jar fully.qualified.ClassName
|
||||
* /foo/bar/baz/bin fully.qualified.ClassName
|
||||
* </pre>
|
||||
*
|
||||
* @param path the jar or dir path
|
||||
* @param className the fully qualified class name
|
||||
* @return the class if it is an extension point
|
||||
*/
|
||||
private static Class<?> loadExtensionPoint(String path, String className) {
|
||||
|
||||
if (getExtensionPointSuffix(className) == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClassLoader classLoader = getClassLoader(path);
|
||||
|
||||
try {
|
||||
Class<?> c = Class.forName(className, true, classLoader);
|
||||
if (isClassOfInterest(c)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
processClassLoadError(path, className, t);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ClassLoader getClassLoader(String path) {
|
||||
ClassLoader classLoader = ClassSearcher.class.getClassLoader();
|
||||
if (!IS_USING_RESTRICTED_EXTENSIONS) {
|
||||
return classLoader; // custom extension class loader is disabled
|
||||
}
|
||||
|
||||
ExtensionDetails extension = ExtensionUtils.getExtension(path);
|
||||
if (extension != null) {
|
||||
log.trace(() -> "Installing custom extension class loader for: " +
|
||||
Json.toStringFlat(extension));
|
||||
classLoader = new ExtensionModuleClassLoader(extension);
|
||||
}
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
private static void processClassLoadError(String path, String name, Throwable t) {
|
||||
|
||||
if (t instanceof LinkageError) {
|
||||
// We see this sometimes when loading classes that match our naming convention for
|
||||
// extension points, but are actually extending 3rd party libraries. For now, do
|
||||
// not make noise in the log for this case.
|
||||
log.trace("LinkageError loading class {}; Incompatible class version? ", name, t);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(t instanceof ClassNotFoundException)) {
|
||||
log.error("Error loading class {} - {}", name, t.getMessage(), t);
|
||||
return;
|
||||
}
|
||||
|
||||
processClassNotFoundExcepetion(path, name, (ClassNotFoundException) t);
|
||||
}
|
||||
|
||||
private static void processClassNotFoundExcepetion(String path, String name,
|
||||
ClassNotFoundException t) {
|
||||
|
||||
if (!isModuleEntryMissingFromClasspath(path)) {
|
||||
// not sure if this can actually happen--it implies a half-built Eclipse issue
|
||||
log.error("Error loading class {} - {}", name, t.getMessage(), t);
|
||||
return;
|
||||
}
|
||||
|
||||
// We have a special case: we know a module class was loaded, but it is not in our
|
||||
// classpath. This can happen in Eclipse when we scan all modules, but the launcher does
|
||||
// not include all modules.
|
||||
if (SystemUtilities.isInTestingMode()) {
|
||||
// ignore the error in testing mode, as many modules are not loaded for any given test
|
||||
return;
|
||||
}
|
||||
|
||||
log.error("Module class is missing from the classpath.\n\tUpdate your launcher " +
|
||||
"accordingly.\n\tModule: '" + path + "'\n\tClass: '" + name + "'");
|
||||
}
|
||||
|
||||
private static boolean isModuleEntryMissingFromClasspath(String path) {
|
||||
|
||||
boolean inModule = ModuleUtilities.isInModule(path);
|
||||
if (!inModule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String classPath = System.getProperty("java.class.path");
|
||||
boolean inClassPath = classPath.contains(path);
|
||||
return !inClassPath;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user