diff --git a/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java b/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java index 8e540e7e5b..ab90f04894 100644 --- a/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/AskValuesExampleScript.java @@ -17,7 +17,6 @@ // @category Examples import ghidra.app.script.GhidraScript; import ghidra.features.base.values.GhidraValuesMap; -import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; import ghidra.util.MessageType; @@ -28,20 +27,12 @@ public class AskValuesExampleScript extends GhidraScript { GhidraValuesMap values = new GhidraValuesMap(); values.defineString("Name"); - values.defineAddress("Address", currentProgram); values.defineInt("Count"); values.defineInt("Max Results", 100); values.defineChoice("Priority", "Low", "Low", "Medium", "High"); - - // When asking for a program, you must supply a consumer that you will use - // to release the program. Since programs share open instances, Ghidra uses - // consumers to keep track of these uses. Scripts can just add themselves - // as the consumer (The askProgram() method does this for you). It is - // important to release it when you are done with. Optionally, you can also - // provide a tool in which case the program will also be opened in the tool (and the - // tool would then also add itself as a consumer). Otherwise, the program will not - // show up in the tool and when you release the consumer, it will be closed. - values.defineProgram("Other Program", this, state.getTool()); + values.defineProgram("Other Program"); + values.defineProjectFile("Project File"); + values.defineProjectFolder("Project Folder"); // Optional validator that can be set to validate values before the dialog returns. It // is called when the "Ok" button is pushed and must return true before the dialog exits. @@ -70,14 +61,23 @@ public class AskValuesExampleScript extends GhidraScript { //dialog. The values map returned may or may not be the same instance as the one passed in. String name = values.getString("Name"); - Address address = values.getAddress("Address"); int age = values.getInt("Count"); int max = values.getInt("Max Results"); String priority = values.getChoice("Priority"); - Program program = values.getProgram("Other Program"); + + // When asking for a program, you must supply a consumer that you will use + // to release the program. Since programs share open instances, Ghidra uses + // consumers to keep track of these uses. Scripts can just add themselves + // as the consumer (The askProgram() method does this for you). It is + // important to release it when you are done with it. Optionally, you can also + // provide a tool in which case the program will also be opened in the tool (and the + // tool would then also add itself as a consumer). Otherwise, the program will not + // show up in the tool and when you release the consumer, it will be closed. + // NOTE: if you call getProgram() more than once, the consumer will be added multiple times + // and you must release it multiple times + Program program = values.getProgram("Other Program", this, state.getTool()); println("Name = " + name); - println("Address = " + address); println("Count = " + age); println("Max Results = " + max); println("Priority = " + priority); 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 45ade2409d..df2004606b 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 @@ -2360,6 +2360,7 @@ public abstract class GhidraScript extends FlatProgramAPI { public GhidraValuesMap askValues(String title, String optionalMessage, GhidraValuesMap values) throws CancelledException { + values.setTaskMonitor(monitor); for (AbstractValue value : values.getValues()) { String key = join(title, value.getName()); loadAskValue(value.getValue(), s -> value.setAsText(s), key); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java index 239d44fc7a..867c496dfb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessAnalyzer.java @@ -39,6 +39,7 @@ import ghidra.framework.*; import ghidra.framework.client.ClientUtil; import ghidra.framework.client.RepositoryAdapter; import ghidra.framework.data.*; +import ghidra.framework.main.AppInfo; import ghidra.framework.model.*; import ghidra.framework.project.DefaultProject; import ghidra.framework.project.DefaultProjectManager; @@ -424,6 +425,7 @@ public class HeadlessAnalyzer { if (locator.getProjectDir().exists()) { project = openProject(locator); + AppInfo.setActiveProject(project); } else { if (options.runScriptsNoImport) { @@ -440,6 +442,7 @@ public class HeadlessAnalyzer { Msg.info(this, "Creating " + (options.deleteProject ? "temporary " : "") + "project: " + locator); project = getProjectManager().createProject(locator, null, false); + AppInfo.setActiveProject(project); } try { @@ -457,6 +460,7 @@ public class HeadlessAnalyzer { } finally { project.close(); + AppInfo.setActiveProject(null); if (!options.runScriptsNoImport && options.deleteProject) { FileUtilities.deleteDir(locator.getProjectDir()); locator.getMarkerFile().delete(); @@ -1546,8 +1550,7 @@ public class HeadlessAnalyzer { // Analyze the primary program, and determine if we should save. // TODO: Analyze non-primary programs (GP-2965). - boolean doSave = - analyzeProgram(fsrl.toString(), primaryProgram) && !options.readOnly; + boolean doSave = analyzeProgram(fsrl.toString(), primaryProgram) && !options.readOnly; // The act of marking the program as temporary by a script will signal // us to discard any changes @@ -1706,7 +1709,7 @@ public class HeadlessAnalyzer { } } } - + private boolean processAsFS(FSRL fsrl, String folderPath, int depth) throws CancelledException { try (FileSystemRef fsRef = fsService.probeFileForFilesystem(fsrl, TaskMonitor.DUMMY, FileSystemProbeConflictResolver.CHOOSEFIRST)) { @@ -1746,7 +1749,6 @@ public class HeadlessAnalyzer { storage.clear(); - if (inputDirFiles != null && !inputDirFiles.isEmpty()) { Msg.info(this, "REPORT: Processing input files: "); Msg.info(this, " project: " + project.getProjectLocator()); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectBrowserPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/AbstractProjectBrowserPanel.java similarity index 52% rename from Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectBrowserPanel.java rename to Ghidra/Features/Base/src/main/java/ghidra/features/base/values/AbstractProjectBrowserPanel.java index 4975c71919..47e71fe92f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectBrowserPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/AbstractProjectBrowserPanel.java @@ -16,102 +16,91 @@ package ghidra.features.base.values; import java.awt.BorderLayout; +import java.util.Objects; import javax.swing.*; import docking.widgets.button.BrowseButton; -import ghidra.framework.main.AppInfo; import ghidra.framework.main.DataTreeDialog; import ghidra.framework.model.*; import ghidra.framework.store.FileSystem; /** - * Component used by Values that use the DataTreeDialog for picking DomainFiles and DomainFolders + * Base class for either project file chooser or project folder chooser */ -class ProjectBrowserPanel extends JPanel { - private JTextField textField; +abstract class AbstractProjectBrowserPanel extends JPanel { + protected Project project; + protected JTextField textField; private JButton browseButton; - private boolean selectFolders; + private DomainFolder startFolder; + private int type; + protected DomainFileFilter filter = null; - ProjectBrowserPanel(String name, boolean selectFolders) { + AbstractProjectBrowserPanel(int type, Project project, String name, String startPath) { super(new BorderLayout()); - this.selectFolders = selectFolders; + this.type = type; + this.project = Objects.requireNonNull(project); + this.startFolder = parseDomainFolder(project, startPath); setName(name); textField = new JTextField(20); browseButton = new BrowseButton(); - browseButton.addActionListener(e -> showDomainFileChooser()); + browseButton.addActionListener(e -> showProjectChooser()); add(textField, BorderLayout.CENTER); add(browseButton, BorderLayout.EAST); } - void setDomainFile(DomainFile value) { - String text = value == null ? "" : value.getPathname(); - textField.setText(text); - } + private void showProjectChooser() { + DataTreeDialog dialog = + new DataTreeDialog(null, "Choose " + getName(), type, filter, project); - void setDomainFolder(DomainFolder value) { - String text = value == null ? "" : value.getPathname(); - textField.setText(text); - } + if (startFolder != null) { + dialog.selectFolder(startFolder); + } + intializeCurrentValue(dialog); - private void showDomainFileChooser() { - DataTreeDialog dialog = new DataTreeDialog(null, "Choose " + getName(), - selectFolders ? DataTreeDialog.CHOOSE_FOLDER : DataTreeDialog.OPEN); dialog.show(); + if (dialog.wasCancelled()) { return; } - String text = selectFolders ? dialog.getDomainFolder().getPathname() - : dialog.getDomainFile().getPathname(); + String text = getSelectedPath(dialog); textField.setText(text); dialog.dispose(); } - DomainFile getDomainFile() { - String text = textField.getText().trim(); - if (text.isBlank()) { - return null; - } - return parseDomainFile(text); - } + protected abstract String getSelectedPath(DataTreeDialog dialog); + + protected abstract void intializeCurrentValue(DataTreeDialog dialog); String getText() { return textField.getText().trim(); } - DomainFolder getDomainFolder() { - String text = textField.getText().trim(); - if (text.isBlank()) { - return parseDomainFolder("/"); - } - return parseDomainFolder(text); - } - - static DomainFile parseDomainFile(String val) { + static DomainFile parseDomainFile(Project project, String val) { // Add the slash to make it an absolute path if (!val.isEmpty() && val.charAt(0) != FileSystem.SEPARATOR_CHAR) { val = FileSystem.SEPARATOR_CHAR + val; } - Project activeProject = AppInfo.getActiveProject(); - DomainFile df = activeProject.getProjectData().getFile(val); + DomainFile df = project.getProjectData().getFile(val); if (df != null) { return df; } return null; } - static DomainFolder parseDomainFolder(String path) { + static DomainFolder parseDomainFolder(Project project, String path) { + if (path == null) { + return null; + } path = path.trim(); // Add the slash to make it an absolute path if (path.isEmpty() || path.charAt(0) != FileSystem.SEPARATOR_CHAR) { path = FileSystem.SEPARATOR_CHAR + path; } - Project activeProject = AppInfo.getActiveProject(); - DomainFolder df = activeProject.getProjectData().getFolder(path); + DomainFolder df = project.getProjectData().getFolder(path); if (df != null) { return df; } return null; } - } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java index 6144698b86..b35d0c5192 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/GhidraValuesMap.java @@ -15,6 +15,8 @@ */ package ghidra.features.base.values; +import java.io.IOException; + import docking.Tool; import docking.widgets.values.GValuesMap; import ghidra.framework.model.DomainFile; @@ -23,12 +25,24 @@ import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressFactory; import ghidra.program.model.lang.LanguageCompilerSpecPair; import ghidra.program.model.listing.Program; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; /** * Extends GValuesMap to add Ghidra specific types such as Address and Program */ public class GhidraValuesMap extends GValuesMap { + private TaskMonitor monitor = TaskMonitor.DUMMY; + /** + * Sets a task monitor to be used when opening programs. Otherwise, {@link TaskMonitor#DUMMY} is + * used. + * @param monitor the TaskMonitor to use for opening programs + */ + public void setTaskMonitor(TaskMonitor monitor) { + this.monitor = TaskMonitor.dummyIfNull(monitor); + } //================================================================================================== // Define Value Methods //================================================================================================== @@ -88,36 +102,23 @@ public class GhidraValuesMap extends GValuesMap { } /** - * Defines a value of type Program. This method opens programs using the given - * consumer and must be properly released when it is no longer needed. This is true - * even if the program is also opened in the tool. + * Defines a value of type Program file. * @param name the name for this value - * @param consumer the consumer to be used to open the program - * @param tool if non-null, the program will also be opened in the given tool - * @return the user-selected Program if a program was - * not selected or null. NOTE: It is very important that the program instance - * returned by this method ALWAYS be properly released from the consumer when no - * longer needed (i.e., {@code program.release(consumer) } - failure to - * properly release the program may result in improper project disposal. If the program was - * also opened in the tool, the tool will be a second consumer responsible for its - * own release. + * @return the new ProgramFileValue defined */ - public ProgramValue defineProgram(String name, Object consumer, Tool tool) { - return defineProgram(name, null, consumer, tool); + public ProgramFileValue defineProgram(String name) { + return defineProgram(name, null); } /** - * Defines a value of type Program. + * Defines a value of type Program file. * @param name the name for this value - * @param defaultValue the initial value - * @param consumer the consumer to be used to open the program - * @param tool if non-null, the program will also be opened in the given tool - * @return the new ProgramValue that was defined + * @param startPath the starting folder to display when picking programs from the chooser + * @return the new ProgramFileValue that was defined */ - public ProgramValue defineProgram(String name, Program defaultValue, Object consumer, - Tool tool) { + public ProgramFileValue defineProgram(String name, String startPath) { checkDup(name); - ProgramValue value = new ProgramValue(name, defaultValue, consumer, tool); + ProgramFileValue value = new ProgramFileValue(name, startPath); valuesMap.put(name, value); return value; } @@ -134,12 +135,12 @@ public class GhidraValuesMap extends GValuesMap { /** * Defines a value of type DomainFile (files in a Ghidra project). * @param name the name for this value - * @param defaultValue the initial value + * @param startingPath the initial folder path for the chooser widget * @return the new ProjectFileValue that was defined */ - public ProjectFileValue defineProjectFile(String name, DomainFile defaultValue) { + public ProjectFileValue defineProjectFile(String name, String startingPath) { checkDup(name); - ProjectFileValue value = new ProjectFileValue(name, defaultValue); + ProjectFileValue value = new ProjectFileValue(name, startingPath); valuesMap.put(name, value); return value; } @@ -156,12 +157,12 @@ public class GhidraValuesMap extends GValuesMap { /** * Defines a value of type DomainFolder (files in a Ghidra project). * @param name the name for this value - * @param defaultValue the initial value (can be null) + * @param defaultValuePath the path for the initial value (can be null) * @return the new ProjectFolderValue that was defined */ - public ProjectFolderValue defineProjectFolder(String name, DomainFolder defaultValue) { + public ProjectFolderValue defineProjectFolder(String name, String defaultValuePath) { checkDup(name); - ProjectFolderValue value = new ProjectFolderValue(name, defaultValue); + ProjectFolderValue value = new ProjectFolderValue(name, defaultValuePath); valuesMap.put(name, value); return value; } @@ -192,14 +193,33 @@ public class GhidraValuesMap extends GValuesMap { } /** - * Gets the {@link Program} value for the given name. - * @param name the name of a previously defined project folder value + * Gets (opens) the {@link Program} value for the given name. If the program is already open, + * then the consumer will be added to the program. The caller of this method is responsible + * for calling {@link Program#release(Object)} with the same consumer when it is done using this + * program. Program are only closed after all consumers are released. If multiple calls + * are made to this method, then the consumer will be added multiple times and must be released + * multiple times. + *

+ * The consumer can be any object, but since the consumer's purpose is to keep the program open + * while some object is using it, the object itself is typically passed in as + * the consumer. For example, when used in a script, passing in the java keyword "this" as the + * consumer will make the script itself the consumer. + *

+ * @param name the name of a previously defined program value + * @param consumer the consumer to be used to open the program + * @param tool if non-null, the program will also be opened in the given tool. Note: the + * program will only be added to the tool once even if this method is called multiple times. * @return the project folder value + * @throws VersionException if the Program being opened is an older version than the + * current Ghidra Program version. + * @throws IOException if there is an error accessing the Program's DomainObject + * @throws CancelledException if the operation is cancelled * @throws IllegalArgumentException if the name hasn't been defined as a project folder type */ - public Program getProgram(String name) { - ProgramValue programValue = getValue(name, ProgramValue.class, "Program"); - return programValue.getValue(); + public Program getProgram(String name, Object consumer, Tool tool) + throws VersionException, IOException, CancelledException { + ProgramFileValue programFileValue = getValue(name, ProgramFileValue.class, "Program"); + return programFileValue.openProgram(consumer, tool, monitor); } /** @@ -258,8 +278,8 @@ public class GhidraValuesMap extends GValuesMap { * @throws IllegalArgumentException if the name hasn't been defined as a Program type */ public void setProgram(String name, Program program) { - ProgramValue programValue = getValue(name, ProgramValue.class, "Program"); - programValue.setValue(program); + ProgramFileValue programFileValue = getValue(name, ProgramFileValue.class, "Program"); + programFileValue.setValue(program == null ? null : program.getDomainFile()); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/LanguageValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/LanguageValue.java index df84b283b0..cb1a337938 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/LanguageValue.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/LanguageValue.java @@ -91,33 +91,33 @@ public class LanguageValue extends AbstractValue { /** * Parses a LanguageCompilerSpecPair from a string. * - * @param val The string to parse. + * @param languageString The string to parse. * @return The LanguageCompilerSpecPair parsed from a string or null if the string does * not parse to a known language-compiler pair. - * @throws ValuesMapParseException + * @throws ValuesMapParseException if the value can't be parsed into a LanguageComilerSpecPair */ - public LanguageCompilerSpecPair parseLanguageCompileSpecPair(String val) + public LanguageCompilerSpecPair parseLanguageCompileSpecPair(String languageString) throws ValuesMapParseException { - if (val.isBlank()) { + if (languageString.isBlank()) { return null; } // Split on last colon to get separated languageID and compilerSpecID - int lastColon = val.lastIndexOf(':'); + int lastColon = languageString.lastIndexOf(':'); if (lastColon < 1) { throw new ValuesMapParseException(getName(), "Language/Compiler Spec", - "Could not parse \"" + val + "\"."); + "Could not parse \"" + languageString + "\"."); } Set languages = getLanguagesCompilerPairs(); - String langId = val.substring(0, lastColon); - String compilerId = val.substring(lastColon + 1); + String langId = languageString.substring(0, lastColon); + String compilerId = languageString.substring(lastColon + 1); LanguageCompilerSpecPair storedLCS = new LanguageCompilerSpecPair(langId, compilerId); if (!languages.contains(storedLCS)) { throw new ValuesMapParseException(getName(), "Language/Compiler Spec", - "Unknown language/Compiler Pair for \"" + val + "\""); + "Unknown language/Compiler Pair for \"" + languageString + "\""); } return storedLCS; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramFileValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramFileValue.java new file mode 100644 index 0000000000..e9e0490f77 --- /dev/null +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramFileValue.java @@ -0,0 +1,116 @@ +/* ### + * 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.features.base.values; + +import java.io.IOException; + +import javax.swing.JTextField; + +import docking.Tool; +import docking.widgets.values.*; +import ghidra.app.services.ProgramManager; +import ghidra.framework.main.AppInfo; +import ghidra.framework.main.DataTreeDialog; +import ghidra.framework.model.DomainFile; +import ghidra.framework.model.Project; +import ghidra.program.model.listing.Program; +import ghidra.util.exception.CancelledException; +import ghidra.util.exception.VersionException; +import ghidra.util.task.TaskMonitor; + +/** + * Value class for {@link Program} files. The editor component consists of the {@link JTextField} + * and a browse button for bringing up a {@link DataTreeDialog} for picking programs from the + * current project. This class also provides a convenience method for opening a program. + *

+ * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily + * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). + * Typically, these values are created indirectly using a {@link GValuesMap} which is then + * given to the constructor of the dialog. However, an alternate approach is to create the + * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} + * method directly. + */ +public class ProgramFileValue extends ProjectFileValue { + + /** + * Constructor for creating a new ProgramFileValue with the given name. + * @param name the name of the value + */ + public ProgramFileValue(String name) { + this(name, null); + } + + /** + * Constructor for creating a new ProgramFileValue with the given name and a starting + * folder when using the project file chooser. + * @param name the name of the value + * @param startingPath the path to a starting folder + */ + public ProgramFileValue(String name, String startingPath) { + this(name, AppInfo.getActiveProject(), startingPath); + } + + /** + * Constructor for ProgramValue when wanting to pick from a different project than the + * active project, such as a read-only project. + * @param name the name of the value + * @param project The project from which to pick a project. + * @param startingPath the path to a starting folder (Can also be a path to program) + */ + public ProgramFileValue(String name, Project project, String startingPath) { + super(name, project, startingPath, Program.class); + } + + /** + * Convenience method for opening the program for the current program file value. If the program + * is already open, then the consumer will be added to the program. The caller of this method is + * responsible for calling {@link Program#release(Object)} with the same consumer when it is + * done using this program. Program are only closed after all consumers are released. If + * multiple calls are made to this method, then the consumer will be added multiple times + * and must be released multiple times. + *

+ * The consumer can be any object, but since the consumer's purpose is to keep the program open + * while some object is using it, the object itself is typically passed in as + * the consumer. For example, when used in a script, passing in the java keyword "this" as the + * consumer will make the script itself the consumer. + *

+ * @param consumer the consumer to be used to open the program + * @param tool optional tool that if non-null, the program will also be opened in the tool + * @param monitor task monitor for cancelling the open program. + * @return a program for the current program value value. If the current program file value + * is null, then null will be returned. + * @throws VersionException if the Program being opened is an older version than the + * current Ghidra Program version. + * @throws IOException if there is an error accessing the Program's DomainObject + * @throws CancelledException if the operation is cancelled + */ + public Program openProgram(Object consumer, Tool tool, TaskMonitor monitor) + throws VersionException, IOException, CancelledException { + + DomainFile domainFile = getValue(); + if (domainFile == null) { + return null; + } + + Program program = (Program) domainFile.getDomainObject(consumer, false, false, monitor); + + if (tool != null && program != null) { + tool.getService(ProgramManager.class).openProgram(program); + } + return program; + } + +} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramValue.java deleted file mode 100644 index d5ef2e5e18..0000000000 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProgramValue.java +++ /dev/null @@ -1,153 +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.features.base.values; - -import java.io.IOException; - -import javax.swing.JComponent; -import javax.swing.JTextField; - -import docking.Tool; -import docking.widgets.values.*; -import ghidra.app.services.ProgramManager; -import ghidra.framework.main.DataTreeDialog; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainObject; -import ghidra.program.model.listing.Program; -import ghidra.util.exception.CancelledException; -import ghidra.util.exception.VersionException; -import ghidra.util.task.TaskMonitor; - -/** - * Value class for {@link Program}s. The editor component consists of the {@link JTextField} and - * a browse button for bringing up a {@link DataTreeDialog} for picking programs from the - * current project. - *

- * This class and other subclasses of {@link AbstractValue} are part of a subsystem for easily - * defining a set of values that can be displayed in an input dialog ({@link ValuesMapDialog}). - * Typically, these values are created indirectly using a {@link GValuesMap} which is then - * given to the constructor of the dialog. However, an alternate approach is to create the - * dialog without a ValuesMap and then use its {@link ValuesMapDialog#addValue(AbstractValue)} - * method directly. - */ -public class ProgramValue extends AbstractValue { - - private ProjectBrowserPanel domainFilePanel; - private Tool tool; - private Object consumer; - - /** - * Construct for ProgramValue - * @param name the name of the value - * @param consumer the program consumer to be used to open a program - * @param tool if non null, the program will also be opened in this tool - */ - public ProgramValue(String name, Object consumer, Tool tool) { - this(name, null, consumer, tool); - } - - /** - * Construct for ProgramValue - * @param name the name of the value - * @param defaultValue the program to use as the default value - * @param consumer the program consumer to be used to open a program - * @param tool if non null, the program will also be opened in this tool - */ - public ProgramValue(String name, Program defaultValue, Object consumer, Tool tool) { - super(name, defaultValue); - this.consumer = consumer; - this.tool = tool; - } - - @Override - public JComponent getComponent() { - if (domainFilePanel == null) { - domainFilePanel = new ProjectBrowserPanel(getName(), false); - } - return domainFilePanel; - } - - @Override - protected void updateValueFromComponent() throws ValuesMapParseException { - if (domainFilePanel != null) { - DomainFile domainFile = domainFilePanel.getDomainFile(); - if (domainFile == null) { - String text = domainFilePanel.getText(); - if (text.isBlank()) { - setValue(null); - return; - } - throw new ValuesMapParseException(getName(), "Program", - "No file found for \"" + text + "\""); - } - Program program = openProgram(domainFile); - setValue(program); - } - } - - private Program openProgram(DomainFile domainFile) throws ValuesMapParseException { - if (domainFile == null) { - return null; - } - Class domainObjectClass = domainFile.getDomainObjectClass(); - if (!Program.class.isAssignableFrom(domainObjectClass)) { - return null; - } - try { - Program program = - (Program) domainFile.getDomainObject(consumer, false, false, TaskMonitor.DUMMY); - - if (tool != null && program != null) { - tool.getService(ProgramManager.class).openProgram(program); - } - return program; - } - catch (VersionException | CancelledException | IOException e) { - throw new ValuesMapParseException(getName(), "Program", e.getMessage()); - } - } - - @Override - protected void updateComponentFromValue() { - Program program = getValue(); - DomainFile df = program == null ? null : program.getDomainFile(); - domainFilePanel.setDomainFile(df); - } - - @Override - protected Program fromString(String valueString) { - DomainFile programFile = ProjectBrowserPanel.parseDomainFile(valueString); - if (programFile == null) { - throw new IllegalArgumentException("Could not find program " + valueString); - } - try { - Program program = openProgram(programFile); - if (program == null) { - throw new IllegalArgumentException("Can't open program: " + valueString); - } - return program; - } - catch (ValuesMapParseException e) { - throw new IllegalArgumentException(e.getMessage()); - } - } - - @Override - protected String toString(Program v) { - return v.getDomainFile().getPathname(); - } - -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFileValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFileValue.java index 7af04f7690..a928e4e8ea 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFileValue.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFileValue.java @@ -21,12 +21,11 @@ import javax.swing.JTextField; import docking.widgets.values.*; import ghidra.framework.main.AppInfo; import ghidra.framework.main.DataTreeDialog; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.Project; +import ghidra.framework.model.*; import ghidra.framework.store.FileSystem; /** - * Value class for project files ({@link DomainFile}). The editor component consists of the + * Value class for project files ({@link DomainFile}). The editor component consists of a * {@link JTextField} and a browse button for bringing up a {@link DataTreeDialog} for picking * project files from the current project. *

@@ -39,20 +38,61 @@ import ghidra.framework.store.FileSystem; */ public class ProjectFileValue extends AbstractValue { - private ProjectBrowserPanel domainFilePanel; + private ProjectFileBrowserPanel domainFilePanel; + private String startingPath; + private Project project; + private Class projectFileClass; + /** + * Constructor for creating a new ProjectFileValue with the given name. + * @param name the name of the value + */ public ProjectFileValue(String name) { - this(name, null); + this(name, AppInfo.getActiveProject(), null, DomainObject.class); } - public ProjectFileValue(String name, DomainFile defaultValue) { - super(name, defaultValue); + /** + * Constructor for creating a new ProgramFileValue with the given name and {@link DomainObject} + * class to filter on (All other types will be filtered out in the chooser). + * @param name the name of the value + * @param projectFileClass the DomainObject class to filter + */ + public ProjectFileValue(String name, Class projectFileClass) { + this(name, AppInfo.getActiveProject(), null, projectFileClass); + } + + /** + * Constructor for creating a new ProjectFileValue with the given name and a starting + * folder when using the project file chooser. + * @param name the name of the value + * @param startingPath the path to a starting folder + */ + public ProjectFileValue(String name, String startingPath) { + this(name, AppInfo.getActiveProject(), startingPath, DomainObject.class); + } + + /** + * Constructor for ProgramValue when wanting to pick from a different project than the + * active project, such as a read-only project. + * @param name the name of the value + * @param project The project from which to pick a project. + * @param startingPath the path to a starting folder (Can also be a path to program) + * @param projectFileClass a {@link DomainFile} class to filter on. (Only those types + * will appear in the chooser) + */ + public ProjectFileValue(String name, Project project, String startingPath, + Class projectFileClass) { + super(name, null); + this.project = project; + this.startingPath = startingPath; + this.projectFileClass = projectFileClass; } @Override public JComponent getComponent() { if (domainFilePanel == null) { - domainFilePanel = new ProjectBrowserPanel(getName(), false); + domainFilePanel = + new ProjectFileBrowserPanel(project, getName(), startingPath, projectFileClass); } return domainFilePanel; } @@ -70,6 +110,11 @@ public class ProjectFileValue extends AbstractValue { throw new ValuesMapParseException(getName(), "Project File", "No file found for \"" + text + "\""); } + Class domainObjectClass = domainFile.getDomainObjectClass(); + if (!projectFileClass.isAssignableFrom(domainObjectClass)) { + throw new ValuesMapParseException(getName(), "Project File", + "Selected path is not a " + projectFileClass.getSimpleName()); + } setValue(domainFile); } } @@ -87,6 +132,11 @@ public class ProjectFileValue extends AbstractValue { if (df == null) { throw new IllegalArgumentException("Can't find domain file: " + valueString); } + Class domainObjectClass = df.getDomainObjectClass(); + if (!projectFileClass.isAssignableFrom(domainObjectClass)) { + throw new IllegalArgumentException( + "Specified file path is not a " + projectFileClass.getSimpleName()); + } return df; } @@ -110,4 +160,44 @@ public class ProjectFileValue extends AbstractValue { protected String toString(DomainFile v) { return v.getPathname(); } + + /** + * Component used by ProjectFileValues for picking project files + */ + class ProjectFileBrowserPanel extends AbstractProjectBrowserPanel { + + ProjectFileBrowserPanel(Project project, String name, String startPath, + Class projectFileClass) { + super(DataTreeDialog.OPEN, project, name, startPath); + filter = df -> projectFileClass.isAssignableFrom(df.getDomainObjectClass()); + } + + void setDomainFile(DomainFile value) { + String text = value == null ? "" : value.getPathname(); + textField.setText(text); + } + + @Override + protected void intializeCurrentValue(DataTreeDialog dialog) { + DomainFile current = getDomainFile(); + if (current != null) { + dialog.selectDomainFile(current); + dialog.setNameText(current.getName()); + } + } + + @Override + protected String getSelectedPath(DataTreeDialog dialog) { + return dialog.getDomainFile().getPathname(); + } + + DomainFile getDomainFile() { + String text = textField.getText().trim(); + if (text.isBlank()) { + return null; + } + return parseDomainFile(project, text); + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFolderValue.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFolderValue.java index 742a284b61..1abb02ff64 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFolderValue.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/values/ProjectFolderValue.java @@ -19,9 +19,9 @@ import javax.swing.JComponent; import javax.swing.JTextField; import docking.widgets.values.*; +import ghidra.framework.main.AppInfo; import ghidra.framework.main.DataTreeDialog; -import ghidra.framework.model.DomainFile; -import ghidra.framework.model.DomainFolder; +import ghidra.framework.model.*; /** * Value class for project folders ({@link DomainFile}). The editor component consists of the @@ -37,20 +37,42 @@ import ghidra.framework.model.DomainFolder; */ public class ProjectFolderValue extends AbstractValue { - private ProjectBrowserPanel domainFilePanel; + private Project project; + private ProjectFolderBrowserPanel domainFilePanel; + /** + * Constructor for ProjectFolderValues with the given name. + * @param name the name of the value + */ public ProjectFolderValue(String name) { this(name, null); } - public ProjectFolderValue(String name, DomainFolder defaultValue) { - super(name, defaultValue); + /** + * Constructor for creating a new ProjectFolderValue with the given name and a path + * for a default folder value. + * @param name the name of the value + * @param defaultValuePath the path for a default folder value + */ + public ProjectFolderValue(String name, String defaultValuePath) { + this(name, AppInfo.getActiveProject(), defaultValuePath); + } + + /** + * Constructor for creating ProjectFolderValues for projects other than the active project. + * @param name the name of the value + * @param project the project to find a folder from + * @param defaultValuePath the path of a default folder value + */ + public ProjectFolderValue(String name, Project project, String defaultValuePath) { + super(name, AbstractProjectBrowserPanel.parseDomainFolder(project, defaultValuePath)); + this.project = project; } @Override public JComponent getComponent() { if (domainFilePanel == null) { - domainFilePanel = new ProjectBrowserPanel(getName(), true); + domainFilePanel = new ProjectFolderBrowserPanel(project, getName(), null); } return domainFilePanel; } @@ -83,7 +105,7 @@ public class ProjectFolderValue extends AbstractValue { @Override protected DomainFolder fromString(String valueString) { - DomainFolder df = ProjectBrowserPanel.parseDomainFolder(valueString); + DomainFolder df = AbstractProjectBrowserPanel.parseDomainFolder(project, valueString); if (df == null) { throw new IllegalArgumentException("Can't find domain folder: " + valueString); } @@ -95,4 +117,38 @@ public class ProjectFolderValue extends AbstractValue { return v.getPathname(); } + /** + * Component used by ProjectFolderValues for picking project folders + */ + class ProjectFolderBrowserPanel extends AbstractProjectBrowserPanel { + + ProjectFolderBrowserPanel(Project project, String name, String startPath) { + super(DataTreeDialog.CHOOSE_FOLDER, project, name, startPath); + } + + void setDomainFolder(DomainFolder value) { + String text = value == null ? "" : value.getPathname(); + textField.setText(text); + } + + @Override + protected void intializeCurrentValue(DataTreeDialog dialog) { + DomainFolder current = getDomainFolder(); + dialog.selectFolder(current); + } + + @Override + protected String getSelectedPath(DataTreeDialog dialog) { + return dialog.getDomainFolder().getPathname(); + } + + DomainFolder getDomainFolder() { + String text = textField.getText().trim(); + if (text.isBlank()) { + return parseDomainFolder(project, "/"); + } + return parseDomainFolder(project, text); + } + } + } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java index f798c1b33a..2755f84f72 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/DataTreeDialog.java @@ -91,10 +91,11 @@ public class DataTreeDialog extends DialogComponentProvider private ProjectDataNewFolderAction newFolderAction; private Integer treeSelectionMode; + private final Project project; /** - * Construct a new DataTreeDialog. This chooser will show all project files. - * Following linked-folders will only be allowed if a type of {@link #CHOOSE_FOLDER} + * Construct a new DataTreeDialog for the active project. This chooser will show all project + * files. Following linked-folders will only be allowed if a type of {@link #CHOOSE_FOLDER} * or {@link #OPEN} is specified. If different behavior is required a filter should * be specified using the other constructor. * @@ -104,11 +105,11 @@ public class DataTreeDialog extends DialogComponentProvider * @throws IllegalArgumentException if invalid type is specified */ public DataTreeDialog(Component parent, String title, int type) { - this(parent, title, type, getDefaultFilter(type)); + this(parent, title, type, getDefaultFilter(type), AppInfo.getActiveProject()); } /** - * Construct a new DataTreeDialog. + * Construct a new DataTreeDialog for the active project. * * @param parent dialog's parent * @param title title to use @@ -117,7 +118,23 @@ public class DataTreeDialog extends DialogComponentProvider * @throws IllegalArgumentException if invalid type is specified */ public DataTreeDialog(Component parent, String title, int type, DomainFileFilter filter) { + this(parent, title, type, filter, AppInfo.getActiveProject()); + } + + /** + * Construct a new DataTreeDialog for the given project. + * + * @param parent dialog's parent + * @param title title to use + * @param type specify OPEN, SAVE, CHOOSE_FOLDER, or CHOOSE_USER_FOLDER + * @param filter filter used to control what is displayed in the data tree + * @param project the project to browse + * @throws IllegalArgumentException if invalid type is specified + */ + public DataTreeDialog(Component parent, String title, int type, DomainFileFilter filter, + Project project) { super(title, true, true, true, false); + this.project = project; this.parent = parent; initDataTreeDialog(type, filter); } @@ -230,7 +247,6 @@ public class DataTreeDialog extends DialogComponentProvider comboModelInitialized = true; // repopulate the tree - Project project = AppInfo.getActiveProject(); ProjectData pd = project.getProjectData(); treePanel.setProjectData(project.getName(), pd); @@ -373,7 +389,7 @@ public class DataTreeDialog extends DialogComponentProvider nameField.setText(domainFolder.getName()); } else { - domainFolder = AppInfo.getActiveProject().getProjectData().getRootFolder(); + domainFolder = project.getProjectData().getRootFolder(); folderNameLabel.setText(domainFolder.getPathname()); nameField.setText(domainFolder.getName()); } @@ -389,7 +405,7 @@ public class DataTreeDialog extends DialogComponentProvider else { domainFolder = treePanel.getSelectedDomainFolder(); if (domainFolder == null) { - domainFolder = AppInfo.getActiveProject().getProjectData().getRootFolder(); + domainFolder = project.getProjectData().getRootFolder(); } folderNameLabel.setText(domainFolder.getPathname()); @@ -415,7 +431,6 @@ public class DataTreeDialog extends DialogComponentProvider return; } - Project project = AppInfo.getActiveProject(); ProjectLocator projectLocator = projectLocators[index]; ProjectData pd = project.getProjectData(projectLocator); String projectName = projectLocator.getName(); @@ -511,7 +526,6 @@ public class DataTreeDialog extends DialogComponentProvider JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); - Project project = AppInfo.getActiveProject(); ProjectData pd = project.getProjectData(); treePanel = new ProjectDataTreePanel(project.getName(), true, // is for the active project @@ -646,7 +660,6 @@ public class DataTreeDialog extends DialogComponentProvider } private void populateProjectModel() { - Project project = AppInfo.getActiveProject(); ProjectLocator[] views = project.getProjectViews(); projectLocators = new ProjectLocator[views.length + 1]; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AbstractValueIntegrationTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AbstractValueIntegrationTest.java index 04ef8ef681..ca3d095c12 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AbstractValueIntegrationTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/AbstractValueIntegrationTest.java @@ -15,6 +15,8 @@ */ package ghidra.features.base.values; +import static org.junit.Assert.*; + import javax.swing.JButton; import javax.swing.JTextField; @@ -25,13 +27,14 @@ import docking.DialogComponentProvider; import docking.DockingWindowManager; import docking.widgets.values.AbstractValue; import docking.widgets.values.ValuesMapDialog; -import ghidra.features.base.values.GhidraValuesMap; -import ghidra.features.base.values.ProjectBrowserPanel; +import ghidra.features.base.values.ProjectFileValue.ProjectFileBrowserPanel; +import ghidra.features.base.values.ProjectFolderValue.ProjectFolderBrowserPanel; +import ghidra.framework.main.AppInfo; import ghidra.framework.main.DataTreeDialog; import ghidra.framework.model.*; import ghidra.program.database.ProgramBuilder; -import ghidra.program.database.ProgramDB; import ghidra.program.model.address.*; +import ghidra.program.model.listing.Program; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.TestEnv; import ghidra.util.task.TaskMonitor; @@ -43,30 +46,47 @@ public abstract class AbstractValueIntegrationTest extends AbstractGhidraHeadedI protected TestEnv env; protected DomainFolder rootFolder; - protected DomainFolder folder; + protected DomainFolder folderA; + protected DomainFolder folderB; protected DomainFile fileA; protected DomainFile fileB; - protected ProgramDB programA; - protected ProgramDB programB; + protected DomainFile fileC; + protected Program programA; + protected Program programB; + protected Project project; @Before public void setup() throws Exception { env = new TestEnv(); - Project project = env.getProject(); - runSwing(() -> env.getFrontEndTool().setActiveProject(project)); + project = env.getProject(); + AppInfo.setActiveProject(project); rootFolder = project.getProjectData().getRootFolder(); - folder = rootFolder.createFolder("A"); - ProgramBuilder programBuilderA = new ProgramBuilder("A", ProgramBuilder._TOY); - ProgramBuilder programBuilderB = new ProgramBuilder("B", ProgramBuilder._TOY); + folderA = rootFolder.createFolder("A"); + folderB = rootFolder.createFolder("B"); + ProgramBuilder programBuilderA = new ProgramBuilder("A", ProgramBuilder._TOY, this); + ProgramBuilder programBuilderB = new ProgramBuilder("B", ProgramBuilder._TOY, this); + ProgramBuilder programBuilderC = new ProgramBuilder("C", ProgramBuilder._TOY, this); programA = programBuilderA.getProgram(); programB = programBuilderB.getProgram(); - fileA = folder.createFile("A", programA, TaskMonitor.DUMMY); - fileB = folder.createFile("B", programB, TaskMonitor.DUMMY); + Program programC = programBuilderC.getProgram(); + fileA = folderA.createFile("A", programA, TaskMonitor.DUMMY); + fileB = folderA.createFile("B", programB, TaskMonitor.DUMMY); + fileC = folderA.createFile("C", programC, TaskMonitor.DUMMY); + programBuilderC.dispose(); // closes program C + programC.release(this); + assertTrue(programC.isClosed()); } @After public void tearDown() { + // some tests close the programs + if (!programA.isClosed()) { + programA.release(this); + } + if (!programB.isClosed()) { + programB.release(this); + } env.dispose(); } @@ -91,7 +111,7 @@ public abstract class AbstractValueIntegrationTest extends AbstractGhidraHeadedI } protected void setProjectFileOnProjectTree(AbstractValue value, DomainFile file) { - ProjectBrowserPanel projectWidget = (ProjectBrowserPanel) value.getComponent(); + ProjectFileBrowserPanel projectWidget = (ProjectFileBrowserPanel) value.getComponent(); pressButtonByName(projectWidget, "BrowseButton", false); DataTreeDialog dataTreeDialog = waitForDialogComponent(DataTreeDialog.class); @@ -104,7 +124,7 @@ public abstract class AbstractValueIntegrationTest extends AbstractGhidraHeadedI } protected void setProjectFolderOnProjectTree(AbstractValue value, DomainFolder folder) { - ProjectBrowserPanel projectWidget = (ProjectBrowserPanel) value.getComponent(); + ProjectFolderBrowserPanel projectWidget = (ProjectFolderBrowserPanel) value.getComponent(); pressButtonByName(projectWidget, "BrowseButton", false); DataTreeDialog dataTreeDialog = waitForDialogComponent(DataTreeDialog.class); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramFileValueTest.java similarity index 54% rename from Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramValueTest.java rename to Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramFileValueTest.java index d78e1603d2..3ec8026472 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramValueTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProgramFileValueTest.java @@ -20,16 +20,15 @@ import static org.junit.Assert.*; import org.junit.Test; import ghidra.app.services.ProgramManager; -import ghidra.features.base.values.ProgramValue; import ghidra.framework.plugintool.PluginTool; import ghidra.program.model.listing.Program; -public class ProgramValueTest extends AbstractValueIntegrationTest { +public class ProgramFileValueTest extends AbstractValueIntegrationTest { private static final String NAME = "Program"; @Test - public void testProgramValueNoDefault() { - values.defineProgram(NAME, this, null); + public void testProgramValueNoDefault() throws Exception { + values.defineProgram(NAME); assertTrue(values.isDefined(NAME)); assertFalse(values.hasValue(NAME)); @@ -37,38 +36,41 @@ public class ProgramValueTest extends AbstractValueIntegrationTest { values.setProgram(NAME, programA); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME)); + assertEquals(programA, values.getProgram(NAME, this, null)); } @Test - public void testProgramValueWithDefault() { - values.defineProgram(NAME, programA, this, null); + public void testSetValue() throws Exception { + values.defineProgram(NAME); + values.setProgram(NAME, programA); assertTrue(values.isDefined(NAME)); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME)); + assertEquals(programA, values.getProgram(NAME, this, null)); values.setProgram(NAME, programB); assertTrue(values.hasValue(NAME)); - assertEquals(programB, values.getProgram(NAME)); + assertEquals(programB, values.getProgram(NAME, this, null)); values.setProgram(NAME, null); assertFalse(values.hasValue(NAME)); } @Test - public void testGetAsText() { - ProgramValue value1 = new ProgramValue(NAME, this, null); - ProgramValue value2 = new ProgramValue(NAME, programA, this, null); - assertNull(value1.getAsText()); - assertEquals("/A/A", value2.getAsText()); + public void testGetAsText() throws Exception { + ProgramFileValue value = values.defineProgram(NAME); + assertNull(value.getAsText()); + + values.setProgram(NAME, programA); + + assertEquals("/A/A", value.getAsText()); } @Test public void testSetAsText() { - ProgramValue v = new ProgramValue(NAME, this, null); - assertEquals(programA, v.setAsText("/A/A")); + ProgramFileValue v = new ProgramFileValue(NAME); + assertEquals(programA.getDomainFile(), v.setAsText("/A/A")); try { v.setAsText(null); fail("Expected exception"); @@ -86,20 +88,20 @@ public class ProgramValueTest extends AbstractValueIntegrationTest { } @Test - public void testNoDefaultValueWithNoDialogInput() { - values.defineProgram(NAME, this, null); + public void testNoDefaultValueWithNoDialogInput() throws Exception { + values.defineProgram(NAME); assertFalse(values.hasValue(NAME)); showDialogOnSwingWithoutBlocking(); pressOk(); assertFalse(values.hasValue(NAME)); - assertNull(values.getProgram(NAME)); + assertNull(values.getProgram(NAME, this, null)); } @Test - public void testNoDefaultValueWithDialogInput() { - values.defineProgram(NAME, this, null); + public void testNoDefaultValueWithDialogInput() throws Exception { + values.defineProgram(NAME); assertFalse(values.hasValue(NAME)); showDialogOnSwingWithoutBlocking(); @@ -107,49 +109,67 @@ public class ProgramValueTest extends AbstractValueIntegrationTest { pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME)); + assertEquals(programA, values.getProgram(NAME, this, null)); } @Test - public void testDefaultValueWithNoDialogInput() { - values.defineProgram(NAME, programA, this, null); - assertTrue(values.hasValue(NAME)); + public void testExistingValueWithNoDialogInput() throws Exception { + values.defineProgram(NAME); + values.setProgram(NAME, programA); showDialogOnSwingWithoutBlocking(); pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(programA, values.getProgram(NAME)); + assertEquals(programA, values.getProgram(NAME, this, null)); } @Test - public void testDefaultValueWithDialogInput() { - values.defineProgram(NAME, programA, this, null); - assertTrue(values.hasValue(NAME)); + public void testDefaultValueWithDialogInput() throws Exception { + values.defineProgram(NAME); + values.setProgram(NAME, programA); showDialogOnSwingWithoutBlocking(); setProjectFileOnProjectTree(values.getAbstractValue(NAME), programB.getDomainFile()); pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(programB, values.getProgram(NAME)); + assertEquals(programB, values.getProgram(NAME, this, null)); } @Test - public void testOpenProgramInTool() { + public void testOpenProgramInTool() throws Exception { PluginTool tool = env.createDefaultTool(); ProgramManager programManagerService = tool.getService(ProgramManager.class); Program[] allOpenPrograms = programManagerService.getAllOpenPrograms(); assertEquals(0, allOpenPrograms.length); - values.defineProgram(NAME, this, tool); + values.defineProgram(NAME); showDialogOnSwingWithoutBlocking(); setProjectFileOnProjectTree(values.getAbstractValue(NAME), programA.getDomainFile()); pressOk(); + Program p = values.getProgram(NAME, this, tool); + allOpenPrograms = programManagerService.getAllOpenPrograms(); assertEquals(1, allOpenPrograms.length); - assertEquals(programA, allOpenPrograms[0]); + assertEquals(p, allOpenPrograms[0]); } + @Test + public void testOpenProgramMutltipleTimes() throws Exception { + values.defineProgram(NAME); + assertEquals(1, programA.getConsumerList().size()); + showDialogOnSwingWithoutBlocking(); + setProjectFileOnProjectTree(values.getAbstractValue(NAME), programA.getDomainFile()); + pressOk(); + + Program p1 = values.getProgram(NAME, this, null); + assertEquals(2, programA.getConsumerList().size()); + Program p2 = values.getProgram(NAME, this, null); + assertEquals(p1, p2); + assertEquals(3, programA.getConsumerList().size()); + p1.release(this); + p2.release(this); + } } diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFileValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFileValueTest.java index 9dd86d5a48..786fe0dea3 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFileValueTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFileValueTest.java @@ -19,8 +19,6 @@ import static org.junit.Assert.*; import org.junit.Test; -import ghidra.features.base.values.ProjectFileValue; - public class ProjectFileValueTest extends AbstractValueIntegrationTest { private static final String NAME = "Project File"; @@ -39,7 +37,8 @@ public class ProjectFileValueTest extends AbstractValueIntegrationTest { @Test public void testProjectFileValueWithDefault() { - values.defineProjectFile(NAME, fileA); + values.defineProjectFile(NAME); + values.setProjectFile(NAME, fileA); assertTrue(values.isDefined(NAME)); assertTrue(values.hasValue(NAME)); @@ -57,7 +56,9 @@ public class ProjectFileValueTest extends AbstractValueIntegrationTest { @Test public void testGetAsText() { ProjectFileValue value1 = new ProjectFileValue(NAME); - ProjectFileValue value2 = new ProjectFileValue(NAME, fileA); + ProjectFileValue value2 = new ProjectFileValue(NAME); + value2.setValue(fileA); + assertNull(value1.getAsText()); assertEquals("/A/A", value2.getAsText()); } @@ -109,7 +110,8 @@ public class ProjectFileValueTest extends AbstractValueIntegrationTest { @Test public void testDefaultValueWithNoDialogInput() { - values.defineProjectFile(NAME, fileA); + values.defineProjectFile(NAME); + values.setProjectFile(NAME, fileA); assertTrue(values.hasValue(NAME)); showDialogOnSwingWithoutBlocking(); @@ -121,7 +123,8 @@ public class ProjectFileValueTest extends AbstractValueIntegrationTest { @Test public void testDefaultValueWithDialogInput() { - values.defineProjectFile(NAME, fileA); + values.defineProjectFile(NAME); + values.setProjectFile(NAME, fileA); assertTrue(values.hasValue(NAME)); showDialogOnSwingWithoutBlocking(); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFolderValueTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFolderValueTest.java index 6104094f95..f17d1334f0 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFolderValueTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/features/base/values/ProjectFolderValueTest.java @@ -19,8 +19,6 @@ import static org.junit.Assert.*; import org.junit.Test; -import ghidra.features.base.values.ProjectFolderValue; - public class ProjectFolderValueTest extends AbstractValueIntegrationTest { private static final String NAME = "Project File"; @@ -31,24 +29,24 @@ public class ProjectFolderValueTest extends AbstractValueIntegrationTest { assertTrue(values.isDefined(NAME)); assertFalse(values.hasValue(NAME)); - values.setProjectFolder(NAME, folder); + values.setProjectFolder(NAME, folderA); assertTrue(values.hasValue(NAME)); - assertEquals(folder, values.getProjectFolder(NAME)); + assertEquals(folderA, values.getProjectFolder(NAME)); } @Test public void testProjectFolderValueWithDefault() { - values.defineProjectFolder(NAME, rootFolder); + values.defineProjectFolder(NAME, "/A"); assertTrue(values.isDefined(NAME)); assertTrue(values.hasValue(NAME)); - assertEquals(rootFolder, values.getProjectFolder(NAME)); + assertEquals(folderA, values.getProjectFolder(NAME)); - values.setProjectFolder(NAME, folder); + values.setProjectFolder(NAME, folderB); assertTrue(values.hasValue(NAME)); - assertEquals(folder, values.getProjectFolder(NAME)); + assertEquals(folderB, values.getProjectFolder(NAME)); values.setProjectFolder(NAME, null); assertFalse(values.hasValue(NAME)); @@ -57,7 +55,7 @@ public class ProjectFolderValueTest extends AbstractValueIntegrationTest { @Test public void testGetAsText() { ProjectFolderValue value1 = new ProjectFolderValue(NAME); - ProjectFolderValue value2 = new ProjectFolderValue(NAME, folder); + ProjectFolderValue value2 = new ProjectFolderValue(NAME, "/A"); assertNull(value1.getAsText()); assertEquals("/A", value2.getAsText()); } @@ -65,7 +63,7 @@ public class ProjectFolderValueTest extends AbstractValueIntegrationTest { @Test public void testSetAsText() { ProjectFolderValue v = new ProjectFolderValue(NAME); - assertEquals(folder, v.setAsText("/A")); + assertEquals(folderA, v.setAsText("/A")); try { v.setAsText(null); fail("Expected exception"); @@ -102,36 +100,36 @@ public class ProjectFolderValueTest extends AbstractValueIntegrationTest { assertFalse(values.hasValue(NAME)); showDialogOnSwingWithoutBlocking(); - setProjectFolderOnProjectTree(values.getAbstractValue(NAME), folder); + setProjectFolderOnProjectTree(values.getAbstractValue(NAME), folderA); pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(folder, values.getProjectFolder(NAME)); + assertEquals(folderA, values.getProjectFolder(NAME)); } @Test public void testDefaultValueWithNoDialogInput() { - values.defineProjectFolder(NAME, folder); + values.defineProjectFolder(NAME, "/A"); assertTrue(values.hasValue(NAME)); showDialogOnSwingWithoutBlocking(); pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(folder, values.getProjectFolder(NAME)); + assertEquals(folderA, values.getProjectFolder(NAME)); } @Test public void testDefaultValueWithDialogInput() { - values.defineProjectFolder(NAME, rootFolder); + values.defineProjectFolder(NAME, "/A"); assertTrue(values.hasValue(NAME)); showDialogOnSwingWithoutBlocking(); - setProjectFolderOnProjectTree(values.getAbstractValue(NAME), folder); + setProjectFolderOnProjectTree(values.getAbstractValue(NAME), folderB); pressOk(); assertTrue(values.hasValue(NAME)); - assertEquals(folder, values.getProjectFolder(NAME)); + assertEquals(folderB, values.getProjectFolder(NAME)); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/AbstractValue.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/AbstractValue.java index 34fce45f78..e8410f667f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/AbstractValue.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/AbstractValue.java @@ -40,6 +40,7 @@ import javax.swing.JComponent; public abstract class AbstractValue { private final String name; private T value; + private T originalValue; /** * Constructor that assigned a name and optional initial value for this object. @@ -49,6 +50,7 @@ public abstract class AbstractValue { protected AbstractValue(String name, T defaultValue) { this.name = Objects.requireNonNull(name); this.value = defaultValue; + this.originalValue = defaultValue; } /** @@ -116,6 +118,13 @@ public abstract class AbstractValue { return value == null ? null : toString(value); } + /** + * Resets the value to its original value when constructed + */ + protected void reset() { + value = originalValue; + } + protected String toString(T t) { return t.toString(); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/GValuesMap.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/GValuesMap.java index d848a8953b..4d24a70cd9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/GValuesMap.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/GValuesMap.java @@ -553,4 +553,14 @@ public class GValuesMap { } } + /** + * Resets the values back to their original values when constructed. Used by the dialog + * when the user cancels. + */ + protected void reset() { + for (AbstractValue inputValue : valuesMap.values()) { + inputValue.reset(); + } + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapDialog.java index 049e7d6839..6dbcdf9d44 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/values/ValuesMapDialog.java @@ -133,6 +133,7 @@ public class ValuesMapDialog extends DialogComponentProvider { @Override protected void cancelCallback() { + valuesMap.reset(); cancelled = true; super.cancelCallback(); } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java index 3c2fef7ed5..2b75e7162a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainFileProxy.java @@ -313,11 +313,6 @@ public class DomainFileProxy implements DomainFile { return true; } - public boolean isUsedExclusivelyBy(Object consumer) { - DomainObjectAdapter dobj = getDomainObject(); - return dobj != null ? dobj.isUsedExclusivelyBy(consumer) : false; - } - @Override public ArrayList getConsumers() { DomainObjectAdapter dobj = getDomainObject(); diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java index 6a02a0c234..c391c727fd 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/DomainObjectAdapter.java @@ -333,19 +333,12 @@ public abstract class DomainObjectAdapter implements DomainObject { @Override public boolean addConsumer(Object consumer) { - if (consumer == null) { - throw new IllegalArgumentException("Consumer must not be null"); - } + Objects.requireNonNull(consumer); synchronized (consumers) { if (isClosed()) { return false; } - - if (consumers.contains(consumer)) { - throw new IllegalArgumentException("Attempted to acquire the " + - "domain object more than once by the same consumer: " + consumer); - } consumers.add(consumer); } @@ -359,18 +352,7 @@ public abstract class DomainObjectAdapter implements DomainObject { } /** - * Returns true if the this file is used only by the given consumer - * @param consumer the consumer - * @return true if the this file is used only by the given consumer - */ - boolean isUsedExclusivelyBy(Object consumer) { - synchronized (consumers) { - return (consumers.size() == 1) && (consumers.contains(consumer)); - } - } - - /** - * Returns true if the given tool is using this object. + * Returns true if the given consumer is using this object. */ @Override public boolean isUsedBy(Object consumer) { diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AppInfo.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AppInfo.java index 17e89cbf9d..ab59f1afec 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AppInfo.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/main/AppInfo.java @@ -31,7 +31,7 @@ public class AppInfo { tool = t; } - static void setActiveProject(Project p) { + public static void setActiveProject(Project p) { activeProject = p; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java index f05da0e53c..3c17e4b5b7 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/model/DomainObject.java @@ -17,7 +17,8 @@ package ghidra.framework.model; import java.io.File; import java.io.IOException; -import java.util.*; +import java.util.List; +import java.util.Map; import ghidra.framework.options.Options; import ghidra.util.ReadOnlyException; @@ -216,7 +217,7 @@ public interface DomainObject { * Returns the list of consumers on this domainObject * @return the list of consumers. */ - public ArrayList getConsumerList(); + public List getConsumerList(); /** * Returns true if the given consumer is using (has open) this domain object.