Merge remote-tracking branch 'origin/GT-3547-dragonmacher-patch-dir-fix'

This commit is contained in:
ghidorahrex 2020-02-24 13:47:12 -05:00
commit 9f66126cef
18 changed files with 277 additions and 530 deletions

View file

@ -54,31 +54,6 @@
If the path is a jar file, then classes within the jar file will be used. If the path is a jar file, then classes within the jar file will be used.
</P> </P>
<P>
The <I>User Plugin Jar Directory</I> shows the directory that contains jar files to
search.
<P>
<IMG SRC="images/note.png" />
In addition to the above, Ghidra also searches in the installation directory, in the
<code>&lt;home&gt;/.ghidra/.ghidra-&lt;version&gt;/plugins</code> directory, if it exists.
</P>
<P>
The directories noted above, as well as any found jar files, are added to Ghidra's
classpath. The search order of these paths is:<A name="SearchOrder"></A></P>
</P>
<BLOCKQUOTE>
<OL>
<LI>Jar files in <I>User Plugin Jar Directory</I> (Plugin Path preference)</LI>
<LI>Jar files in the Ghidra <code>plugins</code> installation directory</LI>
<LI><I>User Plugin Paths</I> from the Plugin Paths preference</LI>
</OL>
</BLOCKQUOTE>
<H2>Editing Plugin Paths</H2> <H2>Editing Plugin Paths</H2>
<BLOCKQUOTE> <BLOCKQUOTE>
@ -142,29 +117,6 @@
class that is loaded is the one that you will be using when you run Ghidra.&nbsp;</P> class that is loaded is the one that you will be using when you run Ghidra.&nbsp;</P>
</BLOCKQUOTE> </BLOCKQUOTE>
<H3>Set the User Plugin Jar Directory</H3>
<UL>
<LI>To set the User Plugin Jar Directory,</LI>
</UL>
<BLOCKQUOTE>
<OL>
<LI>Enter the absolute directory path in the <I>User Plugin Jar Directory</I> field, OR
click on the <B>...</B> button to choose a directory from the file system.</LI>
<LI>
Select the <B>Apply or OK</B> button<B>.</B>
<UL>
<LI><B>Apply</B> applies the changes and leaves the dialog up.</LI>
<LI><B>OK</B> applies the changes and dismisses the dialog.&nbsp;</LI>
</UL>
</LI>
</OL>
</BLOCKQUOTE>
<H3>Remove Paths</H3> <H3>Remove Paths</H3>
<UL> <UL>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

@ -924,7 +924,7 @@ public abstract class GhidraScript extends FlatProgramAPI {
public void println(String message) { public void println(String message) {
String decoratedMessage = getScriptName() + "> " + 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)); Msg.info(GhidraScript.class, new ScriptMessage(decoratedMessage));
if (isRunningHeadless()) { if (isRunningHeadless()) {
@ -3700,7 +3700,8 @@ public abstract class GhidraScript extends FlatProgramAPI {
* @see #getRepeatableComment(Address) * @see #getRepeatableComment(Address)
*/ */
public String getRepeatableCommentAsRendered(Address 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(); PluginTool tool = state.getTool();
if (tool != null) { if (tool != null) {
comment = CommentUtils.getDisplayString(comment, currentProgram); comment = CommentUtils.getDisplayString(comment, currentProgram);

View file

@ -20,7 +20,6 @@ import java.util.List;
import generic.jar.ResourceFile; import generic.jar.ResourceFile;
import ghidra.GhidraClassLoader; import ghidra.GhidraClassLoader;
import ghidra.GhidraLauncher;
import ghidra.framework.preferences.Preferences; import ghidra.framework.preferences.Preferences;
import ghidra.net.ApplicationTrustManagerFactory; import ghidra.net.ApplicationTrustManagerFactory;
import ghidra.util.Msg; import ghidra.util.Msg;
@ -62,22 +61,8 @@ public class HeadlessGhidraApplicationConfiguration extends ApplicationConfigura
if (!(ClassLoader.getSystemClassLoader() instanceof GhidraClassLoader)) { if (!(ClassLoader.getSystemClassLoader() instanceof GhidraClassLoader)) {
return; return;
} }
GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader(); GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader();
// Add user jars
String userJarDir = Preferences.getProperty(Preferences.USER_PLUGIN_JAR_DIRECTORY);
if (userJarDir != null) {
GhidraLauncher.findJarsInDir(new ResourceFile(userJarDir)).forEach(
p -> loader.addPath(p));
}
// Add plugins from user settings directory
String userSettingsPath = Application.getUserSettingsDirectory().getAbsolutePath();
String pluginPath = userSettingsPath + File.separatorChar + "plugins";
loader.addPath(pluginPath);
GhidraLauncher.findJarsInDir(new ResourceFile(pluginPath)).forEach(p -> loader.addPath(p));
// Add user plugins
for (String path : Preferences.getPluginPaths()) { for (String path : Preferences.getPluginPaths()) {
loader.addPath(path); loader.addPath(path);
} }

View file

@ -40,11 +40,6 @@ public class Preferences {
*/ */
private final static String USER_PLUGIN_PATH = "UserPluginPath"; private final static String USER_PLUGIN_PATH = "UserPluginPath";
/**
* Preference name of the user plugin jar directory.
*/
public final static String USER_PLUGIN_JAR_DIRECTORY = "UserPluginJarDirectory";
/** /**
* Preference name for the last opened archive directory. * Preference name for the last opened archive directory.
*/ */
@ -194,6 +189,8 @@ public class Preferences {
* <p> * <p>
* Note: all <code>getProperty(...)</code> methods will first check {@link System#getProperty(String)} * Note: all <code>getProperty(...)</code> methods will first check {@link System#getProperty(String)}
* for a value first. This allows users to override preferences from the command-line. * for a value first. This allows users to override preferences from the command-line.
* @param name the property name
* @return the current property value; null if not set
*/ */
public static String getProperty(String name) { public static String getProperty(String name) {
// prefer system properties, which enables uses to override preferences from the command-line // prefer system properties, which enables uses to override preferences from the command-line
@ -210,6 +207,9 @@ public class Preferences {
* <p> * <p>
* Note: all <code>getProperty(...)</code> methods will first check {@link System#getProperty(String)} * Note: all <code>getProperty(...)</code> methods will first check {@link System#getProperty(String)}
* for a value first. This allows users to override preferences from the command-line. * for a value first. This allows users to override preferences from the command-line.
* @param name the property name
* @param defaultValue the default value
* @return the property value; default value if not set
* *
* @see #getProperty(String, String, boolean) * @see #getProperty(String, String, boolean)
*/ */
@ -289,6 +289,7 @@ public class Preferences {
/** /**
* Get the filename that will be used in the store() method. * Get the filename that will be used in the store() method.
* @return the filename
*/ */
public static String getFilename() { public static String getFilename() {
return filename; return filename;
@ -297,7 +298,7 @@ public class Preferences {
/** /**
* Set the filename so that when the store() method is called, the * Set the filename so that when the store() method is called, the
* preferences are written to this file. * preferences are written to this file.
* @param name * @param name the filename
*/ */
public static void setFilename(String name) { public static void setFilename(String name) {
filename = name; filename = name;
@ -305,6 +306,7 @@ public class Preferences {
/** /**
* Store the preferences in a file for the current filename. * Store the preferences in a file for the current filename.
* @return true if the file was written
* @throws RuntimeException if the preferences filename was not set * @throws RuntimeException if the preferences filename was not set
*/ */
public static boolean store() { public static boolean store() {
@ -346,6 +348,7 @@ public class Preferences {
/** /**
* Return the paths in the UserPluginPath property. * Return the paths in the UserPluginPath property.
* Return zero length array if this property is not set. * Return zero length array if this property is not set.
* @return the paths
* *
*/ */
public static String[] getPluginPaths() { public static String[] getPluginPaths() {
@ -359,6 +362,7 @@ public class Preferences {
/** /**
* Set the paths to be used as the UserPluginPath property. * Set the paths to be used as the UserPluginPath property.
* @param paths the paths
*/ */
public static void setPluginPaths(String[] paths) { public static void setPluginPaths(String[] paths) {
if (paths == null || paths.length == 0) { if (paths == null || paths.length == 0) {
@ -376,55 +380,6 @@ public class Preferences {
properties.setProperty(USER_PLUGIN_PATH, sb.toString()); properties.setProperty(USER_PLUGIN_PATH, sb.toString());
} }
/**
* Set the plugin path property.
* @param pathProperty A string of paths separated by {@link File#pathSeparator} characters
*/
public static void setPluginPathProperty(String pathProperty) {
properties.setProperty(USER_PLUGIN_PATH, pathProperty);
}
/**
* Append path to the plugin path.
* @param path the plugin path to add
*/
public static void addPluginPath(String path) {
List<String> list = getPluginPathList();
if (list == null) {
setPluginPaths(new String[] { path });
return;
}
if (!list.contains(path)) {
list.add(path);
String[] p = new String[list.size()];
setPluginPaths(list.toArray(p));
}
}
/**
* Append paths to the plugin path.
* @param paths the plugin paths to add
*/
public static void addPluginPaths(String[] paths) {
List<String> list = getPluginPathList();
if (list == null) {
setPluginPaths(paths);
return;
}
boolean listChanged = false;
for (String path : paths) {
if (!list.contains(path)) {
list.add(path);
listChanged = true;
}
}
// update plugin path property only if we added a path to the list
if (listChanged) {
String[] p = new String[list.size()];
setPluginPaths(list.toArray(p));
}
}
private static List<String> getPluginPathList() { private static List<String> getPluginPathList() {
String path = properties.getProperty(USER_PLUGIN_PATH); String path = properties.getProperty(USER_PLUGIN_PATH);
if (path == null) { if (path == null) {

View file

@ -58,15 +58,15 @@ public class ClassFinder {
if ((lcPath.endsWith(".jar") || lcPath.endsWith(".zip")) && file.exists()) { if ((lcPath.endsWith(".jar") || lcPath.endsWith(".zip")) && file.exists()) {
if (ClassJar.ignoreJar(lcPath)) { if (ClassJar.ignoreJar(lcPath)) {
log.trace("Ignoring jar file: " + path); log.trace("Ignoring jar file: {}", path);
continue; continue;
} }
log.trace("Searching jar file: " + path); log.trace("Searching jar file: {}", path);
classJars.add(new ClassJar(path, monitor)); classJars.add(new ClassJar(path, monitor));
} }
else if (file.isDirectory()) { else if (file.isDirectory()) {
log.trace("Searching classpath directory: " + path); log.trace("Searching classpath directory: {}", path);
classDirs.add(new ClassDir(path, monitor)); classDirs.add(new ClassDir(path, monitor));
} }
} }
@ -107,7 +107,6 @@ public class ClassFinder {
return n1.compareTo(n2); return n1.compareTo(n2);
}); });
return classList; return classList;
} }

View file

@ -23,11 +23,18 @@ import java.util.jar.JarFile;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.io.FilenameUtils;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.framework.preferences.Preferences;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; 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 * Pattern for matching jar files in a module lib dir
@ -36,24 +43,28 @@ class ClassJar {
* <tt>build/libs</tt>, ending in <tt>.jar</tt> (non-capturing) and then * <tt>build/libs</tt>, ending in <tt>.jar</tt> (non-capturing) and then
* grab that dir's parent and the name of the jar file. * 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"); Pattern.compile(".*/(.*)/(?:lib|build/libs)/(.+).jar");
private static final String PATCH_DIR_PATH_FORWARD_SLASHED = getPatchDirPath();
private static final Set<String> USER_PLUGIN_PATHS = loadUserPluginPaths();
private String path; private String path;
private Set<String> classNameList = new HashSet<>();
private Set<Class<?>> classes = new HashSet<>();
ClassJar(String path, TaskMonitor monitor) throws CancelledException { ClassJar(String path, TaskMonitor monitor) throws CancelledException {
this.path = path; this.path = path;
loadUserPluginPaths();
scan(monitor); scanJar(monitor);
} }
@Override
void getClasses(Set<Class<?>> set, TaskMonitor monitor) { void getClasses(Set<Class<?>> set, TaskMonitor monitor) {
checkForDuplicates(set);
set.addAll(classes); set.addAll(classes);
} }
private void scan(TaskMonitor monitor) throws CancelledException { private void scanJar(TaskMonitor monitor) throws CancelledException {
File file = new File(path); File file = new File(path);
@ -83,30 +94,64 @@ class ClassJar {
// //
// //
// Dev Mode // Dev Mode - don't scan 3rd-party jar files
// //
if (pathName.contains("ExternalLibraries")) { if (pathName.contains("ExternalLibraries")) {
return true; return true;
} }
// //
// Production Mode - In production, only module lib jar files are scanned. // Dev Mode - let everything else through
// //
if (isModuleDependencyJar(pathName)) { if (SystemUtilities.isInDevelopmentMode()) {
return false; return false;
} }
//
// Production Mode - old style (before Extensions) of user contributions
//
String forwardSlashedPathName = pathName.replaceAll("\\\\", "/");
if (isUserPluginJar(forwardSlashedPathName)) {
return false;
}
//
// Production Mode - allow users to enter code in the 'patch' directory
//
if (isPatchJar(forwardSlashedPathName)) {
return false;
}
//
// Production Mode - In production, only module lib jar files are scanned
//
if (isModuleDependencyJar(forwardSlashedPathName)) {
return false;
}
// this is typically a 3rd-party jar file
return true; return true;
} }
static boolean isModuleDependencyJar(String pathName) { private static boolean isUserPluginJar(String pathName) {
return USER_PLUGIN_PATHS.contains(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) { if (ClassSearcher.SEARCH_ALL_JARS) {
return true; // this will search all jar files return true; // this will search all jar files
} }
String forwardSlashed = pathName.replaceAll("\\\\", "/"); // Note: the path is expected to be using forward slashes
Matcher matcher = ANY_MODULE_LIB_JAR_FILE_PATTERN.matcher(forwardSlashed); Matcher matcher = ANY_MODULE_LIB_JAR_FILE_PATTERN.matcher(pathName);
if (!matcher.matches()) { if (!matcher.matches()) {
return false; return false;
} }
@ -121,15 +166,14 @@ class ClassJar {
private void processClassFiles(JarEntry entry) { private void processClassFiles(JarEntry entry) {
String name = entry.getName(); String name = entry.getName();
if (!name.endsWith(".class")) { if (!name.endsWith(CLASS_EXT)) {
return; return;
} }
name = name.substring(0, name.indexOf(".class")); name = name.substring(0, name.indexOf(CLASS_EXT));
name = name.replace('/', '.'); name = name.replace('/', '.');
Class<?> c = ClassFinder.loadExtensionPoint(path, name); Class<?> c = ClassFinder.loadExtensionPoint(path, name);
if (c != null) { if (c != null) {
classNameList.add(name);
classes.add(c); classes.add(c);
} }
} }
@ -138,4 +182,27 @@ class ClassJar {
public String toString() { public String toString() {
return path; return path;
} }
private static String getPatchDirPath() {
ApplicationLayout layout = Application.getApplicationLayout();
ResourceFile patchDir = layout.getPatchDir();
if (patchDir == null) {
return "<no patch dir>"; // not in a distribution
}
String patchPath = patchDir.getAbsolutePath();
String forwardSlashed = patchPath.replaceAll("\\\\", "/");
return forwardSlashed;
}
private static Set<String> loadUserPluginPaths() {
Set<String> result = new HashSet<>();
String[] paths = Preferences.getPluginPaths();
for (String pathName : paths) {
// note: lower case because our client uses lower case for paths
String forwardSlashed = pathName.replaceAll("\\\\", "/").toLowerCase();
result.add(forwardSlashed);
}
return Collections.unmodifiableSet(result);
}
} }

View file

@ -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<Class<?>> classes = new HashSet<>();
abstract void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException;
void checkForDuplicates(Set<Class<?>> 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;
}
}
}
}

View file

@ -23,12 +23,11 @@ import ghidra.util.Msg;
import ghidra.util.exception.CancelledException; import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor; import ghidra.util.task.TaskMonitor;
class ClassPackage { class ClassPackage extends ClassLocation {
private static final FileFilter CLASS_FILTER = private static final FileFilter CLASS_FILTER =
pathname -> pathname.getName().endsWith(".class"); pathname -> pathname.getName().endsWith(CLASS_EXT);
private Set<Class<?>> classes = new HashSet<>();
private Set<ClassPackage> children = new HashSet<>(); private Set<ClassPackage> children = new HashSet<>();
private File rootDir; private File rootDir;
private File packageDir; private File packageDir;
@ -88,7 +87,11 @@ class ClassPackage {
return new File(lRootDir, lPackageName.replace('.', File.separatorChar)); return new File(lRootDir, lPackageName.replace('.', File.separatorChar));
} }
@Override
void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException { void getClasses(Set<Class<?>> set, TaskMonitor monitor) throws CancelledException {
checkForDuplicates(set);
set.addAll(classes); set.addAll(classes);
Iterator<ClassPackage> it = children.iterator(); Iterator<ClassPackage> it = children.iterator();

View file

@ -248,7 +248,7 @@ public class ClassSearcher {
monitor.setMessage("Loading classes..."); monitor.setMessage("Loading classes...");
extensionPoints = searcher.getClasses(monitor); extensionPoints = searcher.getClasses(monitor);
log.trace("Found extension classes: " + extensionPoints); log.trace("Found extension classes {}", extensionPoints);
if (extensionPoints.isEmpty()) { if (extensionPoints.isEmpty()) {
throw new AssertException("Unable to location extension points!"); throw new AssertException("Unable to location extension points!");
} }
@ -307,7 +307,6 @@ public class ClassSearcher {
} }
private static void loadExtensionClassesFromJar() { private static void loadExtensionClassesFromJar() {
// there will only be one root in jar file mode!
ResourceFile appRoot = Application.getApplicationRootDirectory(); ResourceFile appRoot = Application.getApplicationRootDirectory();
ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES"); ResourceFile extensionClassesFile = new ResourceFile(appRoot, "EXTENSION_POINT_CLASSES");
try { try {
@ -326,7 +325,8 @@ public class ClassSearcher {
} }
catch (IOException e) { 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!"); 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) { for (ResourceFile moduleRoot : moduleRootDirectories) {
ResourceFile file = new ResourceFile(moduleRoot, "data/ExtensionPoint.manifest"); ResourceFile file = new ResourceFile(moduleRoot, "data/ExtensionPoint.manifest");
@ -361,7 +361,7 @@ public class ClassSearcher {
} }
buffy.append(')'); buffy.append(')');
extensionPointSuffixPattern = Pattern.compile(buffy.toString()); extensionPointSuffixPattern = Pattern.compile(buffy.toString());
log.trace("Using extension point pattern: " + extensionPointSuffixPattern); log.trace("Using extension point pattern: {}", extensionPointSuffixPattern);
} }
static boolean isExtensionPointName(String name) { static boolean isExtensionPointName(String name) {

View file

@ -1,205 +0,0 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.framework.main;
import java.awt.BorderLayout;
import java.awt.event.*;
import java.io.File;
import javax.swing.*;
import docking.DockingUtils;
import docking.options.editor.ButtonPanelFactory;
import docking.widgets.filechooser.GhidraFileChooser;
/**
* Helper class that restricts the width of the textField to the size of the
* scrolled paths list; also provides the listener for the textfield if user
* presses Enter or Tab in a textfield.
*
*/
class BrowsePathPanel extends JPanel {
private boolean changed;
private GhidraFileChooser fileChooser;
private JTextField pathTextField;
private EditPluginPathDialog dialog;
private JButton browseButton;
/**
* Construct a new BrowsePathPanel.
* @param editDialog parent dialog
* @param sizeComp component to use for size in creating text field
* @param button browse button
* @param dirOnly
* @param textFieldLabel
* @param fieldName name of text field component
*/
BrowsePathPanel(EditPluginPathDialog editDialog, ActionListener buttonListener, String fieldName) {
super();
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
dialog = editDialog;
create(fieldName);
addListeners(buttonListener);
}
/**
* Create the components
* @param sizeComp component to use when creating the text field to get the
* size
* @param textFieldLabel label for the field
*/
private void create(String fieldName) {
pathTextField = new JTextField();
pathTextField.setName(fieldName);
pathTextField.setEditable(false);
pathTextField.setBackground(getBackground());
browseButton = ButtonPanelFactory.createButton(ButtonPanelFactory.BROWSE_TYPE);
browseButton.setToolTipText("Choose Directory");
// construct the panel with text field and browse button
JPanel browsePathPanel = new JPanel(new BorderLayout(5, 5));
browsePathPanel.add(pathTextField, BorderLayout.CENTER);
browsePathPanel.add(browseButton, BorderLayout.EAST);
add(browsePathPanel);
}
private void createFileChooser() {
// create the fileChooser this panel will use based on its input criteria
fileChooser = new GhidraFileChooser(dialog.getComponent());
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
fileChooser.setFileSelectionMode(GhidraFileChooser.DIRECTORIES_ONLY);
fileChooser.setApproveButtonToolTipText("Choose Directory With Plugin JAR Files");
fileChooser.setApproveButtonText("Choose JAR Directory");
}
/**
* Add listeners.
* @param listener listener for the browse button
*/
private void addListeners(ActionListener listener) {
browseButton.addActionListener(listener);
pathTextField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
// when Esc or Ctrl-C is pressed, reset the plugin
// jar directory to what is saved in preferences
if (keyCode == KeyEvent.VK_ESCAPE ||
(DockingUtils.isControlModifier(e) && keyCode == KeyEvent.VK_C)) {
dialog.initJarDirectory();
}
else {
dialog.setApplyEnabled(true);
}
}
});
}
String getPath() {
return pathTextField.getText().trim();
}
boolean isChanged() {
return changed;
}
@Override
public boolean hasFocus() {
return pathTextField.hasFocus();
}
@Override
public void requestFocus() {
pathTextField.requestFocus();
pathTextField.selectAll();
}
/**
* Pop up the file chooser.
*/
void showFileChooser() {
if (fileChooser == null) {
createFileChooser();
}
// reset the status message
dialog.setStatusMessage(EditPluginPathDialog.EMPTY_STATUS);
File pluginFile = fileChooser.getSelectedFile();
if (pluginFile != null) {
setPath(pluginFile);
}
else {
pathTextField.requestFocus();
pathTextField.selectAll();
}
}
/**
* Set whether something has changed.
* @param changed true if something changed
*/
void setChanged(boolean changed) {
this.changed = changed;
}
/**
* Set the path field.
* @param path filename for the path field
* @return boolean true if the path is valid
*/
private boolean setPath(File path) {
boolean pathOK = false;
dialog.setStatusMessage(EditPluginPathDialog.EMPTY_STATUS);
if (!path.canRead()) {
pathTextField.selectAll();
dialog.setStatusMessage("Cannot read path: " + path.toString());
}
else {
pathTextField.setText(path.getAbsolutePath());
pathOK = (pathTextField.getText().trim().length() > 0);
}
if (pathOK) {
dialog.setStatusMessage("Press Apply or OK to set JAR directory.");
}
changed = changed || pathOK;
dialog.enableApply();
return pathOK;
}
/**
* sets the text of the text field of the panel without
* any error checking
*/
void setText(String text) {
pathTextField.setText(text);
}
}

View file

@ -18,8 +18,6 @@
package ghidra.framework.main; package ghidra.framework.main;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -34,6 +32,7 @@ import javax.swing.event.ListSelectionListener;
import docking.DialogComponentProvider; import docking.DialogComponentProvider;
import docking.options.editor.ButtonPanelFactory; import docking.options.editor.ButtonPanelFactory;
import docking.widgets.filechooser.GhidraFileChooser; import docking.widgets.filechooser.GhidraFileChooser;
import docking.widgets.filechooser.GhidraFileChooserMode;
import docking.widgets.label.GDLabel; import docking.widgets.label.GDLabel;
import docking.widgets.list.GListCellRenderer; import docking.widgets.list.GListCellRenderer;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
@ -45,20 +44,18 @@ import ghidra.util.filechooser.GhidraFileFilter;
/** /**
* Dialog for editing the Plugin path and Jar directory path preferences. * Dialog for editing the Plugin path and Jar directory path preferences.
*
* <p>The Plugin Path and Jar directory path are locations where Ghidra searches * <p>The Plugin Path and Jar directory path are locations where Ghidra searches
* for plugins to load. The Plugin Path is specified exactly as a Java Classpath * for plugins to load. The Plugin Path is specified exactly as a Java Classpath
* is specified. The Jar directory is searched only for Jar files containing * is specified. When changes are made to these fields in the dialog, the
* Plugins. When changes are made to these fields in the dialog, the
* preferences file is updated and written to disk. The preferences file is * preferences file is updated and written to disk. The preferences file is
* located in the .ghidra directory in the user's home directory. * located in the .ghidra directory in the user's home directory.
* </P> *
* <p> The preferences file also contains the last project that was opened,
* and the positions of the Ghidra Project Window and other tools that were
* running when the user last exited Ghidra.
* </P>
*/ */
class EditPluginPathDialog extends DialogComponentProvider { class EditPluginPathDialog extends DialogComponentProvider {
static final String ADD_DIR_BUTTON_TEXT = "Add Dir ...";
static final String ADD_JAR_BUTTON_TEXT = "Add Jar ...";
private final static int SIDE_MARGIN = 5; private final static int SIDE_MARGIN = 5;
private final static Color INVALID_PATH_COLOR = Color.red.brighter(); private final static Color INVALID_PATH_COLOR = Color.red.brighter();
private final static Color INVALID_SELECTED_PATH_COLOR = Color.pink; private final static Color INVALID_SELECTED_PATH_COLOR = Color.pink;
@ -80,7 +77,6 @@ class EditPluginPathDialog extends DialogComponentProvider {
// gui members needed for dis/enabling and other state-dependent actions // gui members needed for dis/enabling and other state-dependent actions
private JScrollPane scrollPane; // need for preferred size when resizing private JScrollPane scrollPane; // need for preferred size when resizing
private JList<String> pluginPathsList; private JList<String> pluginPathsList;
private BrowsePathPanel jarPathPanel;
private GhidraFileChooser fileChooser; private GhidraFileChooser fileChooser;
private JButton upButton; private JButton upButton;
private JButton downButton; private JButton downButton;
@ -94,7 +90,6 @@ class EditPluginPathDialog extends DialogComponentProvider {
* Creates a non-modal dialog with OK, Apply, Cancel buttons. * Creates a non-modal dialog with OK, Apply, Cancel buttons.
* The OK and Apply buttons will be enabled when user makes unapplied * The OK and Apply buttons will be enabled when user makes unapplied
* changes to the UserPluginPath or UserPluginJarDirectory property values. * changes to the UserPluginPath or UserPluginJarDirectory property values.
* @param parent parent to this dialog
*/ */
EditPluginPathDialog() { EditPluginPathDialog() {
super("Edit Plugin Path", true, false, true, false); super("Edit Plugin Path", true, false, true, false);
@ -134,8 +129,6 @@ class EditPluginPathDialog extends DialogComponentProvider {
// subsequent panels // subsequent panels
mainPanel.add(buildPluginPathsPanel()); mainPanel.add(buildPluginPathsPanel());
mainPanel.add(Box.createVerticalStrut(10)); mainPanel.add(Box.createVerticalStrut(10));
mainPanel.add(buildJarDirectoryPanel());
mainPanel.add(Box.createVerticalStrut(10));
mainPanel.add(Box.createVerticalGlue()); mainPanel.add(Box.createVerticalGlue());
mainPanel.add(statusMessagePanel); mainPanel.add(statusMessagePanel);
mainPanel.invalidate(); mainPanel.invalidate();
@ -147,33 +140,15 @@ class EditPluginPathDialog extends DialogComponentProvider {
return mainPanel; return mainPanel;
} }
/**
* Gets called when the user selects Apply
*/
@Override @Override
protected void applyCallback() { protected void applyCallback() {
// validate the jar path before applying changes, since the user
// is pressing the Apply button to save this setting
String jarPathname = jarPathPanel.getPath();
if (jarPathname.length() > 0) {
File jarPath = new File(jarPathname);
if (!jarPath.isDirectory() || !jarPath.canRead()) {
setStatusMessage("Bad Jar Directory: " + jarPathname);
jarPathPanel.requestFocus();
return;
}
}
// do the things we need to do to handle the applied changes
handleApply(); handleApply();
} }
/**
* Gets called when the user selects Cancel
*/
@Override @Override
protected void cancelCallback() { protected void cancelCallback() {
close(); close();
// reset original state of dialog for next display of dialog // reset original state of dialog for next display of dialog
enableButtons(false); enableButtons(false);
setStatusMessage(EMPTY_STATUS); setStatusMessage(EMPTY_STATUS);
@ -181,17 +156,6 @@ class EditPluginPathDialog extends DialogComponentProvider {
errorMsg = null; errorMsg = null;
} }
/**
* if the jar directory field has focus, don't let the base dialog
* handle it.
*/
@Override
protected void escapeCallback() {
if (!jarPathPanel.hasFocus()) {
super.escapeCallback();
}
}
/** /**
* Gets called when the user selects Ok * Gets called when the user selects Ok
*/ */
@ -206,30 +170,21 @@ class EditPluginPathDialog extends DialogComponentProvider {
} }
/** /**
* re-set the list of paths each time the dialog is shown * Reset the list of paths each time the dialog is shown
* @param tool the tool
*/ */
public void show(PluginTool tool) { public void show(PluginTool tool) {
setPluginPathsListData(Preferences.getPluginPaths()); setPluginPathsListData(Preferences.getPluginPaths());
initJarDirectory(); setApplyEnabled(pluginPathsChanged);
setStatusMessage(EMPTY_STATUS);
// setting the path enables the apply, but we know we haven't // setting the path enables the apply, but we know we haven't
// made any changes yet, so disable // made any changes yet, so disable
setApplyEnabled(false); setApplyEnabled(false);
tool.showDialog(this); tool.showDialog(this);
} }
/** private void setStatusMessage(String msg) {
* Method enableApply.
*/
void enableApply() {
setApplyEnabled(pluginPathsChanged || jarPathPanel.isChanged());
}
void initJarDirectory() {
setApplyEnabled(pluginPathsChanged);
setStatusMessage(EMPTY_STATUS);
}
void setStatusMessage(String msg) {
if (msg == null || msg.length() == 0) { if (msg == null || msg.length() == 0) {
msg = EMPTY_STATUS; msg = EMPTY_STATUS;
} }
@ -237,15 +192,7 @@ class EditPluginPathDialog extends DialogComponentProvider {
statusMessage.invalidate(); statusMessage.invalidate();
} }
/** private void addJarCallback() {
* @see ghidra.util.bean.GhidraDialog#setApplyEnabled(boolean)
*/
@Override
protected void setApplyEnabled(boolean state) {
super.setApplyEnabled(state);
}
void addJarCallback() {
setStatusMessage(EditPluginPathDialog.EMPTY_STATUS); setStatusMessage(EditPluginPathDialog.EMPTY_STATUS);
@ -253,7 +200,7 @@ class EditPluginPathDialog extends DialogComponentProvider {
fileChooser = new GhidraFileChooser(getComponent()); fileChooser = new GhidraFileChooser(getComponent());
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home"))); fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
} }
fileChooser.setFileSelectionMode(GhidraFileChooser.FILES_ONLY); fileChooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
fileChooser.setFileFilter(JAR_FILTER); fileChooser.setFileFilter(JAR_FILTER);
fileChooser.setApproveButtonToolTipText("Choose Plugin Jar File"); fileChooser.setApproveButtonToolTipText("Choose Plugin Jar File");
fileChooser.setApproveButtonText("Add Jar File"); fileChooser.setApproveButtonText("Add Jar File");
@ -277,7 +224,7 @@ class EditPluginPathDialog extends DialogComponentProvider {
} }
} }
void addDirCallback() { private void addDirCallback() {
setStatusMessage(EditPluginPathDialog.EMPTY_STATUS); setStatusMessage(EditPluginPathDialog.EMPTY_STATUS);
@ -285,7 +232,7 @@ class EditPluginPathDialog extends DialogComponentProvider {
fileChooser = new GhidraFileChooser(getComponent()); fileChooser = new GhidraFileChooser(getComponent());
fileChooser.setCurrentDirectory(new File(System.getProperty("user.home"))); fileChooser.setCurrentDirectory(new File(System.getProperty("user.home")));
} }
fileChooser.setFileSelectionMode(GhidraFileChooser.DIRECTORIES_ONLY); fileChooser.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY);
fileChooser.setFileFilter(GhidraFileFilter.ALL); fileChooser.setFileFilter(GhidraFileFilter.ALL);
fileChooser.setApproveButtonToolTipText("Choose Directory with Plugin class Files"); fileChooser.setApproveButtonToolTipText("Choose Directory with Plugin class Files");
fileChooser.setApproveButtonText("Add Directory"); fileChooser.setApproveButtonText("Add Directory");
@ -310,63 +257,31 @@ class EditPluginPathDialog extends DialogComponentProvider {
} }
} }
/**
* Returns an array of pathnames where plugins can be found; used by custom
* class loader when searching for plugins.
*/
private String[] getUserPluginPaths() { private String[] getUserPluginPaths() {
String[] pluginsArray = new String[listModel.size()]; String[] pluginsArray = new String[listModel.size()];
listModel.copyInto(pluginsArray); listModel.copyInto(pluginsArray);
return pluginsArray; return pluginsArray;
} }
/**
* construct the plugin paths button panel
*/
private JPanel buildPluginPathsPanel() { private JPanel buildPluginPathsPanel() {
// create the UP and DOWN arrows panel // create the UP and DOWN arrows panel
upButton = ButtonPanelFactory.createButton(ButtonPanelFactory.ARROW_UP_TYPE); upButton = ButtonPanelFactory.createButton(ButtonPanelFactory.ARROW_UP_TYPE);
upButton.setName("UpArrow"); upButton.setName("UpArrow");
upButton.addActionListener(new ActionListener() { upButton.addActionListener(e -> handleSelection(UP));
@Override
public void actionPerformed(ActionEvent e) {
handleSelection(UP);
}
});
downButton = ButtonPanelFactory.createButton(ButtonPanelFactory.ARROW_DOWN_TYPE); downButton = ButtonPanelFactory.createButton(ButtonPanelFactory.ARROW_DOWN_TYPE);
downButton.setName("DownArrow"); downButton.setName("DownArrow");
downButton.addActionListener(new ActionListener() { downButton.addActionListener(e -> handleSelection(DOWN));
@Override
public void actionPerformed(ActionEvent e) {
handleSelection(DOWN);
}
});
JPanel arrowButtonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); JPanel arrowButtonsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10));
arrowButtonsPanel.add(upButton); arrowButtonsPanel.add(upButton);
arrowButtonsPanel.add(downButton); arrowButtonsPanel.add(downButton);
// create the Add and Remove panel // create the Add and Remove panel
JButton addJarButton = ButtonPanelFactory.createButton("Add Jar..."); JButton addJarButton = ButtonPanelFactory.createButton(ADD_JAR_BUTTON_TEXT);
addJarButton.addActionListener(new ActionListener() { addJarButton.addActionListener(e -> addJarCallback());
@Override JButton addDirButton = ButtonPanelFactory.createButton(ADD_DIR_BUTTON_TEXT);
public void actionPerformed(ActionEvent e) { addDirButton.addActionListener(e -> addDirCallback());
addJarCallback();
}
});
JButton addDirButton = ButtonPanelFactory.createButton("Add Dir...");
addDirButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
addDirCallback();
}
});
removeButton = ButtonPanelFactory.createButton("Remove"); removeButton = ButtonPanelFactory.createButton("Remove");
removeButton.addActionListener(new ActionListener() { removeButton.addActionListener(e -> handleSelection(REMOVE));
@Override
public void actionPerformed(ActionEvent e) {
handleSelection(REMOVE);
}
});
Dimension d = addJarButton.getPreferredSize(); Dimension d = addJarButton.getPreferredSize();
addDirButton.setPreferredSize(d); addDirButton.setPreferredSize(d);
removeButton.setPreferredSize(d); removeButton.setPreferredSize(d);
@ -415,26 +330,6 @@ class EditPluginPathDialog extends DialogComponentProvider {
return pluginPathListPanel; return pluginPathListPanel;
} }
/**
* construct the jar directory panel
*/
private JPanel buildJarDirectoryPanel() {
ActionListener listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
jarPathPanel.showFileChooser();
enableApply();
}
};
jarPathPanel = new BrowsePathPanel(this, listener, "UserPluginJarDirectory");
jarPathPanel.setText(Preferences.getProperty(Preferences.USER_PLUGIN_JAR_DIRECTORY));
jarPathPanel.setBorder(new TitledBorder("User Plugin Jar Directory"));
return jarPathPanel;
}
private void enableButtons(boolean enabled) { private void enableButtons(boolean enabled) {
upButton.setEnabled(enabled); upButton.setEnabled(enabled);
downButton.setEnabled(enabled); downButton.setEnabled(enabled);
@ -452,22 +347,12 @@ class EditPluginPathDialog extends DialogComponentProvider {
// update Ghidra Preferences with new paths // update Ghidra Preferences with new paths
Preferences.setPluginPaths(userPluginPaths); Preferences.setPluginPaths(userPluginPaths);
// Get user Jar directory
String jarDirectoryName = jarPathPanel.getPath();
if (jarDirectoryName.trim().length() == 0) {
jarDirectoryName = null;
}
// update Ghidra Preferences with new Jar path
Preferences.setProperty(Preferences.USER_PLUGIN_JAR_DIRECTORY, jarDirectoryName);
errorMsg = null; errorMsg = null;
// save the new values // save the new values
if (Preferences.store()) { if (Preferences.store()) {
setStatusMessage("Saved plugin paths successfully!"); setStatusMessage("Saved plugin paths successfully!");
// indicate to user all changes have been applied // indicate to user all changes have been applied
setApplyEnabled(false); setApplyEnabled(false);
jarPathPanel.setChanged(false);
Msg.showInfo(getClass(), rootPanel, "Restart Ghidra", Msg.showInfo(getClass(), rootPanel, "Restart Ghidra",
"You must restart Ghidra in order\n" + "for path changes to take effect."); "You must restart Ghidra in order\n" + "for path changes to take effect.");
@ -479,10 +364,6 @@ class EditPluginPathDialog extends DialogComponentProvider {
} }
} }
/**
* dispatched method for handling button actions on the
* dialog
*/
private void handleSelection(byte whichAction) { private void handleSelection(byte whichAction) {
// if nothing selected, nothing to do // if nothing selected, nothing to do
if (selectedInList == null) { if (selectedInList == null) {
@ -574,8 +455,8 @@ class EditPluginPathDialog extends DialogComponentProvider {
private void setPluginPathsListData(String[] pluginPathNames) { private void setPluginPathsListData(String[] pluginPathNames) {
listModel.clear(); listModel.clear();
for (int p = 0; p < pluginPathNames.length; p++) { for (String pluginPathName : pluginPathNames) {
listModel.addElement(pluginPathNames[p]); listModel.addElement(pluginPathName);
} }
} }

View file

@ -64,6 +64,9 @@ public class GhidraApplicationLayout extends ApplicationLayout {
// Extensions // Extensions
extensionInstallationDir = findExtensionInstallationDirectory(); extensionInstallationDir = findExtensionInstallationDirectory();
extensionArchiveDir = findExtensionArchiveDirectory(); extensionArchiveDir = findExtensionArchiveDirectory();
// Patch directory
patchDir = findPatchDirectory();
} }
/** /**
@ -156,8 +159,9 @@ public class GhidraApplicationLayout extends ApplicationLayout {
// Skip classpath entries that live in an application root directory...we've already // Skip classpath entries that live in an application root directory...we've already
// found those. // found those.
if (applicationRootDirs.stream().anyMatch(dir -> FileUtilities.isPathContainedWithin( if (applicationRootDirs.stream()
dir.getFile(false), classpathEntry.getFile(false)))) { .anyMatch(dir -> FileUtilities.isPathContainedWithin(
dir.getFile(false), classpathEntry.getFile(false)))) {
continue; continue;
} }
@ -173,6 +177,24 @@ public class GhidraApplicationLayout extends ApplicationLayout {
return ModuleUtilities.findModules(applicationRootDirs, moduleRootDirectories); return ModuleUtilities.findModules(applicationRootDirs, moduleRootDirectories);
} }
/**
* Returns the directory that allows users to add jar and class files to override existing
* distribution files
* @return the patch dir; null if not in a distribution
*/
protected ResourceFile findPatchDirectory() {
if (SystemUtilities.isInDevelopmentMode()) {
return null;
}
if (applicationInstallationDir == null) {
return null;
}
return new ResourceFile(applicationInstallationDir, "Ghidra/patch");
}
/** /**
* Returns the directory where all Ghidra extension archives are stored. * Returns the directory where all Ghidra extension archives are stored.
* This should be at the following location:<br> * This should be at the following location:<br>

View file

@ -50,9 +50,10 @@ public class GhidraLauncher {
// Get application layout // Get application layout
GhidraApplicationLayout layout = new GhidraApplicationLayout(); GhidraApplicationLayout layout = new GhidraApplicationLayout();
GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader();
// Build the classpath // Build the classpath
List<String> classpathList = new ArrayList<String>(); List<String> classpathList = new ArrayList<>();
Map<String, GModule> modules = getOrderedModules(layout); Map<String, GModule> modules = getOrderedModules(layout);
if (SystemUtilities.isInDevelopmentMode()) { if (SystemUtilities.isInDevelopmentMode()) {
@ -60,13 +61,12 @@ public class GhidraLauncher {
addExternalJarPaths(classpathList, layout.getApplicationRootDirs()); addExternalJarPaths(classpathList, layout.getApplicationRootDirs());
} }
else { else {
addPatchPaths(classpathList, layout.getApplicationInstallationDir()); addPatchPaths(classpathList, layout.getPatchDir());
addModuleJarPaths(classpathList, modules); addModuleJarPaths(classpathList, modules);
} }
classpathList = orderClasspath(classpathList, modules); classpathList = orderClasspath(classpathList, modules);
// Add the classpath to the class loader // Add the classpath to the class loader
GhidraClassLoader loader = (GhidraClassLoader) ClassLoader.getSystemClassLoader();
classpathList.forEach(entry -> loader.addPath(entry)); classpathList.forEach(entry -> loader.addPath(entry));
// Make sure the thing to launch is a GhidraLaunchable // Make sure the thing to launch is a GhidraLaunchable
@ -86,14 +86,21 @@ public class GhidraLauncher {
* Add patch jars to the given path list. This should be done first so they take precedence in * Add patch jars to the given path list. This should be done first so they take precedence in
* the classpath. * the classpath.
* *
* @param pathList The list of paths to add to. * @param pathList The list of paths to add to
* @param installDir The application installation directory. * @param patchDir The application installation directory; may be null
*/ */
private static void addPatchPaths(List<String> pathList, ResourceFile installDir) { private static void addPatchPaths(List<String> pathList, ResourceFile patchDir) {
ResourceFile patchDir = new ResourceFile(installDir, "Ghidra/patch"); if (patchDir == null || !patchDir.exists()) {
if (patchDir.exists()) { return;
pathList.addAll(findJarsInDir(patchDir));
} }
// this will allow for unbundled class files
pathList.add(patchDir.getAbsolutePath());
// this is each jar file, sorted for loading consistency
List<String> jars = findJarsInDir(patchDir);
Collections.sort(jars);
pathList.addAll(jars);
} }
/** /**
@ -103,8 +110,8 @@ public class GhidraLauncher {
* @param modules The modules to get the bin directories of. * @param modules The modules to get the bin directories of.
*/ */
private static void addModuleBinPaths(List<String> pathList, Map<String, GModule> modules) { private static void addModuleBinPaths(List<String> pathList, Map<String, GModule> modules) {
ModuleUtilities.getModuleBinDirectories(modules).forEach( Collection<ResourceFile> dirs = ModuleUtilities.getModuleBinDirectories(modules);
d -> pathList.add(d.getAbsolutePath())); dirs.forEach(d -> pathList.add(d.getAbsolutePath()));
} }
/** /**
@ -114,8 +121,8 @@ public class GhidraLauncher {
* @param modules The modules to get the jars of. * @param modules The modules to get the jars of.
*/ */
private static void addModuleJarPaths(List<String> pathList, Map<String, GModule> modules) { private static void addModuleJarPaths(List<String> pathList, Map<String, GModule> modules) {
ModuleUtilities.getModuleLibDirectories(modules).forEach( Collection<ResourceFile> dirs = ModuleUtilities.getModuleLibDirectories(modules);
d -> pathList.addAll(findJarsInDir(d))); dirs.forEach(d -> pathList.addAll(findJarsInDir(d)));
} }
/** /**
@ -260,12 +267,12 @@ public class GhidraLauncher {
Map<String, GModule> modules) { Map<String, GModule> modules) {
Set<String> fatJars = modules Set<String> fatJars = modules
.values() .values()
.stream() .stream()
.flatMap(m -> m.getFatJars().stream()) .flatMap(m -> m.getFatJars().stream())
.collect(Collectors.toSet()); .collect(Collectors.toSet());
List<String> orderedList = new ArrayList<String>(pathList); List<String> orderedList = new ArrayList<>(pathList);
for (String path : pathList) { for (String path : pathList) {
if (fatJars.contains(new File(path).getName())) { if (fatJars.contains(new File(path).getName())) {

View file

@ -55,4 +55,10 @@ public class GhidraTestApplicationLayout extends GhidraApplicationLayout {
File installDir = new File(getUserTempDir(), "ExtensionInstallDir"); File installDir = new File(getUserTempDir(), "ExtensionInstallDir");
return new ResourceFile(installDir); return new ResourceFile(installDir);
} }
@Override
protected ResourceFile findPatchDirectory() {
File dir = new File(getUserTempDir(), "patch");
return new ResourceFile(dir);
}
} }

View file

@ -42,6 +42,7 @@ public abstract class ApplicationLayout {
protected File userTempDir; protected File userTempDir;
protected File userCacheDir; protected File userCacheDir;
protected File userSettingsDir; protected File userSettingsDir;
protected ResourceFile patchDir;
protected ResourceFile extensionArchiveDir; protected ResourceFile extensionArchiveDir;
protected ResourceFile extensionInstallationDir; protected ResourceFile extensionInstallationDir;
@ -109,10 +110,10 @@ public abstract class ApplicationLayout {
} }
/** /**
* Returns the directory where archived Ghidra Extensions are stored. * Returns the directory where archived application Extensions are stored.
* *
* @return The Ghidra Extensions archive directory. Could be null if the * @return the application Extensions archive directory. Could be null if the
* {@link ApplicationLayout} does not support Ghidra Extensions. * {@link ApplicationLayout} does not support application Extensions.
* *
*/ */
public final ResourceFile getExtensionArchiveDir() { public final ResourceFile getExtensionArchiveDir() {
@ -120,15 +121,24 @@ public abstract class ApplicationLayout {
} }
/** /**
* Returns the Ghidra Extensions installation folder. * Returns the application Extensions installation folder.
* *
* @return The Ghidra Extensions installation directory. Could be null if the * @return the application Extensions installation directory. Could be null if the
* {@link ApplicationLayout} does not support Ghidra Extensions. * {@link ApplicationLayout} does not support application Extensions.
*/ */
public final ResourceFile getExtensionInstallationDir() { public final ResourceFile getExtensionInstallationDir() {
return extensionInstallationDir; return extensionInstallationDir;
} }
/**
* Returns the location of the application patch directory. The patch directory can be
* used to modify existing code within a distribution.
* @return the patch directory; may be null
*/
public final ResourceFile getPatchDir() {
return patchDir;
}
/** /**
* Creates the application's user directories (or ensures they already exist). * Creates the application's user directories (or ensures they already exist).
* *

View file

@ -255,7 +255,6 @@ public class FrontEndPluginScreenShots extends GhidraScreenShotGenerator {
@Test @Test
public void testEditPluginPath() { public void testEditPluginPath() {
Preferences.setProperty(Preferences.USER_PLUGIN_JAR_DIRECTORY, "/MyPlugins");
Preferences.setPluginPaths(new String[] { "/myJar.jar", "/MyPlugins/classes" }); Preferences.setPluginPaths(new String[] { "/myJar.jar", "/MyPlugins/classes" });
performAction("Edit Plugin Path", "FrontEndPlugin", false); performAction("Edit Plugin Path", "FrontEndPlugin", false);
DialogComponentProvider dialog = getDialog(); DialogComponentProvider dialog = getDialog();

View file

@ -1,3 +1,12 @@
Drop jar files in this directory to apply patches to an installation of Ghidra. Any jar files This directory exits so that Ghidra releases can be patched, or overridden.
found in this directory will be placed at the front of the classpath, allowing them to override Classes or jar files placed in this directory will found and loaded
any existing classes in any module. *before* the classes that exist in the release jar files. One exception
is that classes in the Utility module can not be patched in this way.
The jar files will be sorted by name before being prepended to the classpath
in order to have predictable class loading between Ghidra runs. This patch
directory will be the very first patch entry on the classpath such that any
individual classes will be found before classes in any of the patch jar files.
The class files in this directory must be in the standard java package
directory structure.