diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java
index e7305c1178..d1a295363d 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/script/GhidraScript.java
@@ -924,7 +924,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
public void println(String message) {
String decoratedMessage = getScriptName() + "> " + message;
- // note: use a Message object to facilite script message log filtering
+ // note: use a Message object to facilitate script message log filtering
Msg.info(GhidraScript.class, new ScriptMessage(decoratedMessage));
if (isRunningHeadless()) {
@@ -3700,7 +3700,8 @@ public abstract class GhidraScript extends FlatProgramAPI {
* @see #getRepeatableComment(Address)
*/
public String getRepeatableCommentAsRendered(Address address) {
- String comment = currentProgram.getListing().getComment(CodeUnit.REPEATABLE_COMMENT, address);
+ String comment =
+ currentProgram.getListing().getComment(CodeUnit.REPEATABLE_COMMENT, address);
PluginTool tool = state.getTool();
if (tool != null) {
comment = CommentUtils.getDisplayString(comment, currentProgram);
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
index 0a3964972d..4294a9ebab 100644
--- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFinder.java
+++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassFinder.java
@@ -58,15 +58,15 @@ public class ClassFinder {
if ((lcPath.endsWith(".jar") || lcPath.endsWith(".zip")) && file.exists()) {
if (ClassJar.ignoreJar(lcPath)) {
- log.trace("Ignoring jar file: " + path);
+ log.trace("Ignoring jar file: {}", path);
continue;
}
- log.trace("Searching jar file: " + path);
+ log.trace("Searching jar file: {}", path);
classJars.add(new ClassJar(path, monitor));
}
else if (file.isDirectory()) {
- log.trace("Searching classpath directory: " + path);
+ log.trace("Searching classpath directory: {}", path);
classDirs.add(new ClassDir(path, monitor));
}
}
@@ -107,7 +107,6 @@ public class ClassFinder {
return n1.compareTo(n2);
});
-
return classList;
}
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 4200075f27..29c3a29a41 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
@@ -17,17 +17,23 @@ package ghidra.util.classfinder;
import java.io.File;
import java.io.IOException;
-import java.util.*;
+import java.util.Enumeration;
+import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.apache.commons.io.FilenameUtils;
+
+import generic.jar.ResourceFile;
+import ghidra.framework.Application;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
+import utility.application.ApplicationLayout;
-class ClassJar {
+class ClassJar extends ClassLocation {
/**
* Pattern for matching jar files in a module lib dir
@@ -36,24 +42,35 @@ class ClassJar {
* build/libs, ending in .jar (non-capturing) and then
* grab that dir's parent and the name of the jar file.
*/
- private static Pattern ANY_MODULE_LIB_JAR_FILE_PATTERN =
+ private static final Pattern ANY_MODULE_LIB_JAR_FILE_PATTERN =
Pattern.compile(".*/(.*)/(?:lib|build/libs)/(.+).jar");
+ private static final String PATCH_DIR_PATH_FORWARD_SLASHED = getPatchDirPath();
+
+ private static String getPatchDirPath() {
+ ApplicationLayout layout = Application.getApplicationLayout();
+ ResourceFile installDir = layout.getApplicationInstallationDir();
+ ResourceFile patchDir = new ResourceFile(installDir, "Ghidra/patch");
+ String patchPath = patchDir.getAbsolutePath();
+ String forwardSlashed = patchPath.replaceAll("\\\\", "/");
+ return forwardSlashed;
+ }
+
private String path;
- private Set classNameList = new HashSet<>();
- private Set> classes = new HashSet<>();
ClassJar(String path, TaskMonitor monitor) throws CancelledException {
this.path = path;
- scan(monitor);
+ scanJar(monitor);
}
+ @Override
void getClasses(Set> set, TaskMonitor monitor) {
+ checkForDuplicates(set);
set.addAll(classes);
}
- private void scan(TaskMonitor monitor) throws CancelledException {
+ private void scanJar(TaskMonitor monitor) throws CancelledException {
File file = new File(path);
@@ -88,25 +105,39 @@ class ClassJar {
if (pathName.contains("ExternalLibraries")) {
return true;
}
+ //
+ // Production Mode - allow users to enter code in the 'patch' directory
+ //
+ String forwardSlashedPathName = pathName.replaceAll("\\\\", "/");
+ if (isPatchJar(forwardSlashedPathName)) {
+ return false;
+ }
//
// Production Mode - In production, only module lib jar files are scanned.
//
- if (isModuleDependencyJar(pathName)) {
+ if (isModuleDependencyJar(forwardSlashedPathName)) {
return false;
}
return true;
}
- static boolean isModuleDependencyJar(String pathName) {
+ // Note: the path is expected to be using forward slashes
+ private static boolean isPatchJar(String pathName) {
+ String jarDirectory = FilenameUtils.getFullPathNoEndSeparator(pathName);
+ return jarDirectory.equalsIgnoreCase(PATCH_DIR_PATH_FORWARD_SLASHED);
+ }
+
+ // Note: the path is expected to be using forward slashes
+ private static boolean isModuleDependencyJar(String pathName) {
if (ClassSearcher.SEARCH_ALL_JARS) {
return true; // this will search all jar files
}
- String forwardSlashed = pathName.replaceAll("\\\\", "/");
- Matcher matcher = ANY_MODULE_LIB_JAR_FILE_PATTERN.matcher(forwardSlashed);
+ // Note: the path is expected to be using forward slashes
+ Matcher matcher = ANY_MODULE_LIB_JAR_FILE_PATTERN.matcher(pathName);
if (!matcher.matches()) {
return false;
}
@@ -121,15 +152,14 @@ class ClassJar {
private void processClassFiles(JarEntry entry) {
String name = entry.getName();
- if (!name.endsWith(".class")) {
+ if (!name.endsWith(CLASS_EXT)) {
return;
}
- name = name.substring(0, name.indexOf(".class"));
+ name = name.substring(0, name.indexOf(CLASS_EXT));
name = name.replace('/', '.');
Class> c = ClassFinder.loadExtensionPoint(path, name);
if (c != null) {
- classNameList.add(name);
classes.add(c);
}
}
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
new file mode 100644
index 0000000000..0a7e1b3a6f
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/classfinder/ClassLocation.java
@@ -0,0 +1,56 @@
+/* ###
+ * 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.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 {
+
+ protected static final String CLASS_EXT = ".class";
+
+ final Logger log = LogManager.getLogger(getClass());
+
+ protected Set> classes = new HashSet<>();
+
+ abstract void getClasses(Set> set, TaskMonitor monitor) throws CancelledException;
+
+ void checkForDuplicates(Set> existingClasses) {
+ if (!log.isTraceEnabled()) {
+ return;
+ }
+
+ for (Class> c : classes) {
+ if (existingClasses.contains(c)) {
+ Module module = c.getModule();
+ module.toString();
+ log.trace("Attempting to load the same class twice: {}. " +
+ "Keeping loaded class ; ignoring class from {}", c, this);
+ return;
+ }
+ }
+ }
+
+}
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 914da5d02c..ddadfa431e 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,12 +23,11 @@ import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
-class ClassPackage {
+class ClassPackage extends ClassLocation {
private static final FileFilter CLASS_FILTER =
- pathname -> pathname.getName().endsWith(".class");
+ pathname -> pathname.getName().endsWith(CLASS_EXT);
- private Set> classes = new HashSet<>();
private Set children = new HashSet<>();
private File rootDir;
private File packageDir;
@@ -88,7 +87,11 @@ class ClassPackage {
return new File(lRootDir, lPackageName.replace('.', File.separatorChar));
}
+ @Override
void getClasses(Set> set, TaskMonitor monitor) throws CancelledException {
+
+ checkForDuplicates(set);
+
set.addAll(classes);
Iterator it = children.iterator();
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 5235df82c8..4a10c95b36 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
@@ -248,7 +248,7 @@ public class ClassSearcher {
monitor.setMessage("Loading classes...");
extensionPoints = searcher.getClasses(monitor);
- log.trace("Found extension classes: " + extensionPoints);
+ log.trace("Found extension classes {}", extensionPoints);
if (extensionPoints.isEmpty()) {
throw new AssertException("Unable to location extension points!");
}
@@ -307,7 +307,6 @@ public class ClassSearcher {
}
private static void loadExtensionClassesFromJar() {
- // there will only be one root in jar file mode!
ResourceFile appRoot = Application.getApplicationRootDirectory();
ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES");
try {
@@ -326,7 +325,8 @@ public class ClassSearcher {
}
catch (IOException e) {
- throw new AssertException("Got unexpected IOException ", e);
+ throw new AssertException("Unexpected IOException reading extension class file " +
+ extensionClassesFile, e);
}
}
@@ -338,7 +338,7 @@ public class ClassSearcher {
throw new AssertException("Could not find modules for Class Searcher!");
}
- log.trace("Scanning module root directories: " + moduleRootDirectories);
+ log.trace("Scanning module root directories: {}", moduleRootDirectories);
for (ResourceFile moduleRoot : moduleRootDirectories) {
ResourceFile file = new ResourceFile(moduleRoot, "data/ExtensionPoint.manifest");
@@ -361,7 +361,7 @@ public class ClassSearcher {
}
buffy.append(')');
extensionPointSuffixPattern = Pattern.compile(buffy.toString());
- log.trace("Using extension point pattern: " + extensionPointSuffixPattern);
+ log.trace("Using extension point pattern: {}", extensionPointSuffixPattern);
}
static boolean isExtensionPointName(String name) {
diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraClassLoader.java b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraClassLoader.java
index 4cad280222..0521cec150 100644
--- a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraClassLoader.java
+++ b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraClassLoader.java
@@ -31,7 +31,7 @@ import ghidra.util.Msg;
*
*/
public class GhidraClassLoader extends URLClassLoader {
-
+
private static final String CP = "java.class.path";
/**
@@ -45,7 +45,7 @@ public class GhidraClassLoader extends URLClassLoader {
}
@Override
- public void addURL(URL url) {
+ public void addURL(URL url) {
super.addURL(url);
try {
System.setProperty(CP,
@@ -56,6 +56,23 @@ public class GhidraClassLoader extends URLClassLoader {
}
}
+ /**
+ * Places the given path first in the classpath
+ * @param path the path
+ */
+ void prependPath(String path) {
+
+ try {
+ URL url = new File(path).toURI().toURL();
+ super.addURL(url);
+ File file = new File(url.toURI());
+ System.setProperty(CP, file + File.pathSeparator + System.getProperty(CP));
+ }
+ catch (MalformedURLException | URISyntaxException e) {
+ Msg.debug(this, "Invalid path: " + path, e);
+ }
+ }
+
/**
* Converts the specified path to a {@link URL} and adds it to the classpath.
*
diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraLauncher.java b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraLauncher.java
index 5ba84d7ef7..2db3d8f6a1 100644
--- a/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraLauncher.java
+++ b/Ghidra/Framework/Utility/src/main/java/ghidra/GhidraLauncher.java
@@ -50,6 +50,7 @@ public class GhidraLauncher {
// Get application layout
GhidraApplicationLayout layout = new GhidraApplicationLayout();
+ GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader();
// Build the classpath
List classpathList = new ArrayList();
@@ -60,13 +61,12 @@ public class GhidraLauncher {
addExternalJarPaths(classpathList, layout.getApplicationRootDirs());
}
else {
- addPatchPaths(classpathList, layout.getApplicationInstallationDir());
+ addPatchJarPaths(loader, layout.getApplicationInstallationDir());
addModuleJarPaths(classpathList, modules);
}
classpathList = orderClasspath(classpathList, modules);
// Add the classpath to the class loader
- GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader();
classpathList.forEach(entry -> loader.addPath(entry));
// Make sure the thing to launch is a GhidraLaunchable
@@ -83,17 +83,28 @@ public class GhidraLauncher {
}
/**
- * Add patch jars to the given path list. This should be done first so they take precedence in
- * the classpath.
+ * Add patch dir and jars to the given path list. This should be done first so they take
+ * precedence in the classpath.
*
- * @param pathList The list of paths to add to.
+ * @param loader The loader to which paths will be added.
* @param installDir The application installation directory.
*/
- private static void addPatchPaths(List pathList, ResourceFile installDir) {
+ private static void addPatchJarPaths(GhidraClassLoader loader, ResourceFile installDir) {
ResourceFile patchDir = new ResourceFile(installDir, "Ghidra/patch");
- if (patchDir.exists()) {
- pathList.addAll(findJarsInDir(patchDir));
+ if (!patchDir.exists()) {
+ return;
}
+
+ List patchJars = findJarsInDir(patchDir);
+ Collections.sort(patchJars);
+
+ // add in reverse order, since we are prepending
+ for (int i = patchJars.size() - 1; i >= 0; i--) {
+ loader.prependPath(patchJars.get(i));
+ }
+
+ // put last; paths are prepended in list order
+ loader.prependPath(patchDir.getAbsolutePath());
}
/**
@@ -103,8 +114,8 @@ public class GhidraLauncher {
* @param modules The modules to get the bin directories of.
*/
private static void addModuleBinPaths(List pathList, Map modules) {
- ModuleUtilities.getModuleBinDirectories(modules).forEach(
- d -> pathList.add(d.getAbsolutePath()));
+ Collection dirs = ModuleUtilities.getModuleBinDirectories(modules);
+ dirs.forEach(d -> pathList.add(d.getAbsolutePath()));
}
/**
@@ -114,8 +125,8 @@ public class GhidraLauncher {
* @param modules The modules to get the jars of.
*/
private static void addModuleJarPaths(List pathList, Map modules) {
- ModuleUtilities.getModuleLibDirectories(modules).forEach(
- d -> pathList.addAll(findJarsInDir(d)));
+ Collection dirs = ModuleUtilities.getModuleLibDirectories(modules);
+ dirs.forEach(d -> pathList.addAll(findJarsInDir(d)));
}
/**
@@ -260,10 +271,10 @@ public class GhidraLauncher {
Map modules) {
Set fatJars = modules
- .values()
- .stream()
- .flatMap(m -> m.getFatJars().stream())
- .collect(Collectors.toSet());
+ .values()
+ .stream()
+ .flatMap(m -> m.getFatJars().stream())
+ .collect(Collectors.toSet());
List orderedList = new ArrayList(pathList);
diff --git a/GhidraBuild/patch/README.txt b/GhidraBuild/patch/README.txt
index c80aadd7a0..db031468b4 100644
--- a/GhidraBuild/patch/README.txt
+++ b/GhidraBuild/patch/README.txt
@@ -1,3 +1,12 @@
-Drop jar files in this directory to apply patches to an installation of Ghidra. Any jar files
-found in this directory will be placed at the front of the classpath, allowing them to override
-any existing classes in any module.
\ No newline at end of file
+Into this directory may be added compiled Java class files, either inside of a jar file or
+in a directory structure. This directory and the contained jar files will be prepended to
+the classpath, allowing them to override any existing classes in any module.
+
+The jar files will be sorted by name before being added to the classpath in order to present
+predictable class loading between Ghidra runs. This directory will be prepended on the classpath
+before any jar files, given the classes in this directory precedence over the jar files.
+
+The class files in this directory must be in a directory structure that matches their respective
+packages.
+
+