diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoMapSpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoMapSpec.java index b46c2526c6..a6b26ec0f3 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoMapSpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoMapSpec.java @@ -47,6 +47,7 @@ public interface AutoMapSpec extends ExtensionPoint { private Private() { ClassSearcher.addChangeListener(classListener); + classesChanged(null); } private synchronized void classesChanged(ChangeEvent evt) { diff --git a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java index 4af67b7ac8..8a7ef06588 100644 --- a/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java +++ b/Ghidra/Debug/Debugger/src/main/java/ghidra/app/plugin/core/debug/gui/action/AutoReadMemorySpec.java @@ -45,6 +45,7 @@ public interface AutoReadMemorySpec extends ExtensionPoint { private Private() { ClassSearcher.addChangeListener(classListener); + classesChanged(null); } private synchronized void classesChanged(ChangeEvent evt) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java b/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java index bc9a821f0d..ba86f3b449 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/GhidraRun.java @@ -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)"); }); }; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/RuntimeInfoProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/RuntimeInfoProvider.java index ac63281510..86040bb4fa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/RuntimeInfoProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/RuntimeInfoProvider.java @@ -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 map = ClassSearcher.getClasses(ExtensionPoint.class) + JTabbedPane epTabbedPane = new JTabbedPane(); + tabbedPane.add("Extension Points", epTabbedPane); + + // Discovered Potential Extension Points + Map map = ClassSearcher.getExtensionPointInfo() .stream() - .collect(Collectors.toMap(e -> e.getName(), - e -> ClassSearcher.getExtensionPointName(e.getName()))); - String name = "Extension Points"; - tabbedPane.add(new MapTablePanel(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(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(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(name, map, "Name", "Type", 400, true, plugin), name); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java index 7481abc017..cc1ede4243 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/AnalyzeHeadless.java @@ -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 { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/FieldNavigator.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/FieldNavigator.java index 162c550cf0..aa1866f3f4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/FieldNavigator.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/util/FieldNavigator.java @@ -82,9 +82,8 @@ public class FieldNavigator implements ButtonPressedListener, FieldMouseHandlerS new HashMap, List>(); // find all instances of AnnotatedString - List instances = - ClassSearcher.getInstances(FieldMouseHandlerExtension.class); - for (FieldMouseHandlerExtension fieldMouseHandler : instances) { + List instances = ClassSearcher.getInstances(FieldMouseHandler.class); + for (FieldMouseHandler fieldMouseHandler : instances) { addHandler(map, fieldMouseHandler); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java index b716b5ccaa..c3a9b33fdc 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractGhidraHeadlessIntegrationTest.java @@ -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> extentions = - (List>) getInstanceField("extensionPoints", ClassSearcher.class); - Set> set = new HashSet<>(extentions); - Iterator> 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> extensionPointSuffixToInfoMap = + (Map>) getInstanceField("extensionPointSuffixToInfoMap", + ClassSearcher.class); + BidiMap> loadedCache = + (BidiMap>) getInstanceField("loadedCache", ClassSearcher.class); + String suffix = ClassSearcher.getExtensionPointSuffix(service.getSimpleName()); + + if (suffix != null) { + Set 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> newExtensionPoints = new ArrayList<>(set); - setInstanceField("extensionPoints", ClassSearcher.class, newExtensionPoints); } //================================================================================================== diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/GhidraJarBuilder.java b/Ghidra/Features/Base/src/main/java/ghidra/util/GhidraJarBuilder.java index ef2ade34fe..5fe253e0c4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/GhidraJarBuilder.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/GhidraJarBuilder.java @@ -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()); } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java index 34eca7b1e7..f949d44f1c 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptions2Test.java @@ -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 analyzer) { - @SuppressWarnings("unchecked") - List> extensions = - (List>) getInstanceField("extensionPoints", ClassSearcher.class); + Map> extensionPointSuffixToInfoMap = + (Map>) getInstanceField("extensionPointSuffixToInfoMap", + ClassSearcher.class); + BidiMap> loadedCache = + (BidiMap>) getInstanceField("loadedCache", ClassSearcher.class); // remove any traces of previous test runs - extensions.removeIf(c -> c.getSimpleName().contains("TestAnalyzerStub")); + Set 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 -> { diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java index 3798f08c99..19a659a715 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassDir.java @@ -33,7 +33,7 @@ class ClassDir { classPackage = new ClassPackage(dir, "", monitor); } - void getClasses(Set> set, TaskMonitor monitor) throws CancelledException { + void getClasses(Set set, TaskMonitor monitor) throws CancelledException { classPackage.getClasses(set, monitor); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFileInfo.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFileInfo.java new file mode 100644 index 0000000000..56a2ed86f5 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFileInfo.java @@ -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) {} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFinder.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFinder.java deleted file mode 100644 index 7466ae2cf8..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFinder.java +++ /dev/null @@ -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> FILTER_CLASSES = - Collections.unmodifiableList(Arrays.asList(ExtensionPoint.class)); - - private Set classDirs = new HashSet<>(); - private Set classJars = new HashSet<>(); - - public ClassFinder(List searchPaths, TaskMonitor monitor) throws CancelledException { - initialize(searchPaths, monitor); - } - - private void initialize(List searchPaths, TaskMonitor monitor) - throws CancelledException { - - Msg.trace(this, - "Using restricted extension class loader? " + IS_USING_RESTRICTED_EXTENSIONS); - - Set pathSet = new LinkedHashSet<>(searchPaths); - Iterator 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> getClasses(TaskMonitor monitor) throws CancelledException { - - Set> classSet = new HashSet<>(); - - for (ClassDir dir : classDirs) { - monitor.checkCancelled(); - dir.getClasses(classSet, monitor); - } - - for (ClassJar jar : classJars) { - monitor.checkCancelled(); - jar.getClasses(classSet, monitor); - } - - List> 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}. - *

- * Examples: - *

-	 * /foo/bar/baz/file.jar fully.qualified.ClassName
-	 * /foo/bar/baz/bin fully.qualified.ClassName
-	 * 
- * - * @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; - } -} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java index 26c66acdd1..47ac3d4fdc 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassJar.java @@ -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 USER_PLUGIN_PATHS = loadUserPluginPaths(); + private Set 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> set, TaskMonitor monitor) { - checkForDuplicates(set); + public void getClasses(Set 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)); } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassLocation.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassLocation.java index 94b9f6763d..f4b08c43df 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassLocation.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassLocation.java @@ -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> classes = new HashSet<>(); - - protected abstract void getClasses(Set> set, TaskMonitor monitor) - throws CancelledException; - - protected void checkForDuplicates(Set> 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 set, TaskMonitor monitor) throws CancelledException; } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java index 4420266ddf..f32fdc89f9 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassPackage.java @@ -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 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 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> set, TaskMonitor monitor) throws CancelledException { - - checkForDuplicates(set); + public void getClasses(Set set, TaskMonitor monitor) throws CancelledException { set.addAll(classes); diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java index b7c05cec5e..79a78896d1 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassSearcher.java @@ -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> extensionPoints; + private static final boolean IS_USING_RESTRICTED_EXTENSIONS = + Boolean.getBoolean(GhidraClassLoader.ENABLE_RESTRICTED_EXTENSIONS_PROPERTY); + private static List> FILTER_CLASSES = Arrays.asList(ExtensionPoint.class); + private static Pattern extensionPointSuffixPattern; + private static Map> extensionPointSuffixToInfoMap; + private static BidiMap> loadedCache = new DualHashBidiMap<>(); + private static Set falsePositiveCache = new HashSet<>(); + private static volatile boolean hasSearched; + private static volatile boolean isSearching; private static WeakSet 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 List> getClasses(Class c) { - return getClasses(c, null); + public static List> getClasses(Class 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, null 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 List> getClasses(Class c, + public static List> getClasses(Class ancestorClass, Predicate> classFilter) { + if (!hasSearched) { + return List.of(); + } if (isSearching) { throw new IllegalStateException( "Cannot call the getClasses() while the ClassSearcher is searching!"); } - List> 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) extensionPoint))) { - list.add((Class) extensionPoint); + List> 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) c))) { + list.add((Class) 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 List getInstances(Class 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, null 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 List getInstances(Class c, ClassFilter filter) { List> 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. + *

+ * 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 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. + *

+ * 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 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 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. + *

+ * 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 getFalsePositives() { + return falsePositiveCache; + } + + /** + * Gets the given class's extension point suffix. + *

+ * 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> findClasses(TaskMonitor monitor) + throws CancelledException { + log.info("Searching for classes..."); + + Set classDirs = new HashSet<>(); + Set 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 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 void prioritizeClasses(List> 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 gatherSearchPaths() { // @@ -315,7 +492,7 @@ public class ClassSearcher { private static void getPropertyPaths(String property, List 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> loadExtensionClassesFromJar() { ResourceFile appRoot = Application.getApplicationRootDirectory(); ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES"); try { List classNames = FileUtilities.getLines(extensionClassesFile); - List> extensionClasses = new ArrayList<>(); + Set 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 extensionPointSuffixes = new HashSet<>(); Collection 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}. + *

+ * Examples: + *

+	 * /foo/bar/baz/file.jar fully.qualified.ClassName
+	 * /foo/bar/baz/bin fully.qualified.ClassName
+	 * 
+ * + * @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; + } }