From 3f27438af9f836f3af6e80ae5460e96fc589b2b2 Mon Sep 17 00:00:00 2001 From: ghidravore Date: Wed, 23 Dec 2020 12:18:30 -0500 Subject: [PATCH 1/4] GP-544 adding capability to save off analysis options to named configurations that can be used to analyze future programs. This avoids having to always adjust the analysis options from the defaults for every new program. --- .../AnalysisEnablementTableModel.java | 2 +- .../core/analysis/AnalysisOptionsDialog.java | 20 +- .../plugin/core/analysis/AnalysisPanel.java | 353 ++++++++++++++++-- .../core/analysis/AutoAnalysisManager.java | 2 +- .../AnalyzeAllOpenProgramsTaskTest.java | 1 + .../framework/options/FileOptionsTest.java | 132 +++++++ .../ghidra/framework/options/EditorState.java | 30 +- .../framework/options/EditorStateFactory.java | 4 +- .../ghidra/framework/options/FileOptions.java | 156 ++++++++ 9 files changed, 644 insertions(+), 56 deletions(-) create mode 100644 Ghidra/Features/Base/src/test/java/ghidra/framework/options/FileOptionsTest.java create mode 100644 Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/FileOptions.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisEnablementTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisEnablementTableModel.java index a936adca73..82a880e95b 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisEnablementTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisEnablementTableModel.java @@ -90,7 +90,7 @@ public class AnalysisEnablementTableModel Boolean enabled = (Boolean) value; analyzerStates.get(rowIndex).setEnabled(enabled); String analyzerName = analyzerStates.get(rowIndex).getName(); - panel.setAnalyzerEnabled(analyzerName, enabled); + panel.setAnalyzerEnabled(analyzerName, enabled, true); fireTableRowsUpdated(rowIndex, rowIndex); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java index 24981b85ce..9098a00519 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java @@ -67,16 +67,16 @@ public class AnalysisOptionsDialog extends DialogComponentProvider implements @Override public void propertyChange(PropertyChangeEvent evt) { - // On any analyzer status change, update the options for all programs - // being analyzed. This is necessary to keep options consistent across all - // programs being analyzed. - // - // Note: It's possible to receive a property change notification before the - // analysis panel has finished being constructed, so protect against - // that before calling the update method. - if (panel != null) { - panel.updateOptionForAllPrograms(evt.getPropertyName(), (Boolean) evt.getNewValue()); - } +// // On any analyzer status change, update the options for all programs +// // being analyzed. This is necessary to keep options consistent across all +// // programs being analyzed. +// // +// // Note: It's possible to receive a property change notification before the +// // analysis panel has finished being constructed, so protect against +// // that before calling the update method. +// if (panel != null) { +// panel.updateOptionForAllPrograms(evt.getPropertyName(), (Boolean) evt.getNewValue()); +// } } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java index cfb2301424..fa230cf3e9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java @@ -16,7 +16,11 @@ package ghidra.app.plugin.core.analysis; import java.awt.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.beans.*; +import java.io.File; +import java.io.IOException; import java.util.*; import java.util.List; @@ -26,29 +30,44 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.table.TableColumn; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.FilenameUtils; import docking.help.Help; import docking.help.HelpService; import docking.options.editor.GenericOptionsComponent; import docking.widgets.OptionDialog; +import docking.widgets.combobox.GhidraComboBox; import docking.widgets.label.GLabel; import docking.widgets.table.GTable; import ghidra.GhidraOptions; import ghidra.app.services.Analyzer; +import ghidra.framework.Application; import ghidra.framework.options.*; +import ghidra.framework.preferences.Preferences; import ghidra.program.model.listing.Program; import ghidra.util.HelpLocation; +import ghidra.util.Msg; import ghidra.util.exception.AssertException; import ghidra.util.layout.VerticalLayout; +import utilities.util.FileUtilities; -class AnalysisPanel extends JPanel implements PropertyChangeListener { +class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListener { + // create an empty options to represent the defaults of the analyzers + private static final Options STANDARD_DEFAULT_OPTIONS = + new FileOptions("Standard Defaults"); + private static final String OPTIONS_FILE_EXTENSION = "options"; public static final String PROTOTYPE = " (Prototype)"; public final static int COLUMN_ANALYZER_IS_ENABLED = 0; + private static final String ANALYZER_OPTIONS_SAVE_DIR = "AnalyzerDefaults"; + private static final String LAST_DEFAULT_OPTIONS = "LAST_OPTIONS_DEFAULTS"; + private List programs; private PropertyChangeListener propertyChangeListener; private Options analysisOptions; + private Options currentProgramOptions; // this will have all the non-default options from the program + private Options selectedOptions = STANDARD_DEFAULT_OPTIONS; private JTable table; private AnalysisEnablementTableModel model; @@ -61,6 +80,8 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { private EditorStateFactory editorStateFactory; private JPanel noOptionsPanel; + private GhidraComboBox defaultOptionsCombo; + private JButton deleteButton; /** * Constructor @@ -89,15 +110,30 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { if (CollectionUtils.isEmpty(programs)) { throw new AssertException("Must provide a program to run analysis"); } - this.programs = programs; this.propertyChangeListener = propertyChangeListener; this.editorStateFactory = editorStateFactory; analysisOptions = programs.get(0).getOptions(Program.ANALYSIS_PROPERTIES); - + currentProgramOptions = getNonDefaultProgramOptions(); setName("Analysis Panel"); build(); load(); + loadCurrentOptionsIntoEditors(); + } + + /** + * Copies the the non-default options from the program analysis options into a new options object + * @return the the non-default options from the program analysis options into a new options object + */ + private Options getNonDefaultProgramOptions() { + FileOptions options = new FileOptions("Current Program Options"); + List optionNames = analysisOptions.getOptionNames(); + for (String optionName : optionNames) { + if (!analysisOptions.isDefaultValue(optionName)) { + options.putObject(optionName, analysisOptions.getObject(optionName, null)); + } + } + return options; } private void load() { @@ -141,6 +177,14 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { } private void build() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + add(buildMainPanel(), BorderLayout.CENTER); +// add(buildOptionsComboBoxPanel(), BorderLayout.NORTH); +// add(buildMainPanel(), BorderLayout.CENTER); + } + + private JComponent buildMainPanel() { buildTable(); buildAnalyzerOptionsPanel(); @@ -148,9 +192,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, buildLeftPanel(), buildRightPanel()); splitpane.setBorder(null); - setLayout(new BorderLayout()); - setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - add(splitpane, BorderLayout.CENTER); + return splitpane; } private void buildAnalyzerOptionsPanel() { @@ -158,6 +200,21 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { configureBorder(analyzerOptionsPanel, "Options"); } + private Component buildOptionsComboBoxPanel() { + JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + + Options[] defaultOptionsArray = getDefaultOptionsArray(); + defaultOptionsCombo = new GhidraComboBox<>(defaultOptionsArray); + selectedOptions = findOptions(defaultOptionsArray, getLastUsedDefaultOptionsName()); + defaultOptionsCombo.setSelectedItem(selectedOptions); + defaultOptionsCombo.addItemListener(this); + Dimension preferredSize = defaultOptionsCombo.getPreferredSize(); + defaultOptionsCombo.setPreferredSize(new Dimension(200, preferredSize.height)); + panel.add(defaultOptionsCombo); + panel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); + return panel; + } + private Component buildRightPanel() { JSplitPane splitpane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, buildDescriptionPanel(), analyzerOptionsPanel); @@ -185,7 +242,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { } private JPanel buildLeftPanel() { - JPanel buttonPanel = buildButtonPanel(); + JPanel buttonPanel = buildControlPanel(); JScrollPane scrollPane = new JScrollPane(table); scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); @@ -199,20 +256,57 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { return panel; } + private JPanel buildControlPanel() { + JPanel panel = new JPanel(new BorderLayout()); + + panel.add(buildButtonPanel(), BorderLayout.NORTH); + panel.add(buildOptionsComboBoxPanel(), BorderLayout.SOUTH); + + return panel; + } + private JPanel buildButtonPanel() { JButton selectAllButton = new JButton("Select All"); selectAllButton.addActionListener(e -> selectAll()); JButton deselectAllButton = new JButton("Deselect All"); deselectAllButton.addActionListener(e -> deselectAll()); - JButton restoreDefaultsButton = new JButton("Restore Defaults"); - restoreDefaultsButton.addActionListener(e -> restoreDefaults()); + JButton resetButton = new JButton("Reset"); + resetButton.setToolTipText("Resets the editors to the selected options configuration"); + resetButton.addActionListener(e -> loadCurrentOptionsIntoEditors()); + JButton saveButton = new JButton("Save"); + saveButton.setToolTipText("Saves the current editor settings to a named configuration"); + saveButton.addActionListener(e -> saveCurrentOptionsConfiguration()); + deleteButton = new JButton("Delete"); + deleteButton.addActionListener(e -> deleteSelectedOptionsConfiguration()); + deleteButton.setToolTipText("Deletes the currently selected user configuration"); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); buttonPanel.add(selectAllButton); buttonPanel.add(deselectAllButton); - buttonPanel.add(restoreDefaultsButton); + buttonPanel.add(resetButton); + buttonPanel.add(saveButton); + buttonPanel.add(deleteButton); return buttonPanel; } + private void deleteSelectedOptionsConfiguration() { + if (!isUserConfiguration(selectedOptions)) { + // can only delete user configurations + return; + } + String configurationName = selectedOptions.getName(); + int result = OptionDialog.showYesNoDialog(this, "Delete Configuration?", + "Are you sure you want to delete options configuration \"" + configurationName + "\"?"); + if (result != OptionDialog.YES_OPTION) { + return; + } + + File configurationFile = getOptionsSaveFile(configurationName); + configurationFile.delete(); + selectedOptions = currentProgramOptions; + reloadOptionsCombo(currentProgramOptions); + loadCurrentOptionsIntoEditors(); + } + private void selectAll() { int rowCount = model.getRowCount(); for (int i = 0; i < rowCount; ++i) { @@ -227,15 +321,83 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { } } - private void restoreDefaults() { - int answer = OptionDialog.showYesNoDialog(this, "Restore Default Analysis Options", - "Do you really want to restore the analysis options to the default values?"); - if (answer == OptionDialog.YES_OPTION) { - AutoAnalysisManager manager = AutoAnalysisManager.getAnalysisManager(programs.get(0)); - manager.restoreDefaultOptions(); - editorStateFactory.clearAll(); - load(); + private void saveCurrentOptionsConfiguration() { + String defaultSaveName = ""; + if (selectedOptions != STANDARD_DEFAULT_OPTIONS && + selectedOptions != currentProgramOptions) { + defaultSaveName = selectedOptions.getName(); } + String saveName = OptionDialog.showInputSingleLineDialog(this, "Save Configuration", + "Options Configuration Name", defaultSaveName); + if (saveName == null) { + return; + } + saveName = saveName.trim(); + if (saveName.length() == 0) { + return; + } + File saveFile = getOptionsSaveFile(saveName); + FileOptions saved = saveCurrentOptions(); + try { + saved.save(saveFile); + reloadOptionsCombo(saved); + } + catch (IOException e) { + Msg.error(this, "Error Saving Default Options", e); + } + } + + private FileOptions saveCurrentOptions() { + FileOptions saveTo = new FileOptions(""); + List analyzerStates = model.getModelData(); + for (AnalyzerEnablementState analyzerState : analyzerStates) { + String analyzerName = analyzerState.getName(); + boolean enabled = analyzerState.isEnabled(); + if (!Objects.equals(Boolean.valueOf(enabled), + analysisOptions.getDefaultValue(analyzerName))) { + saveTo.setBoolean(analyzerName, enabled); + } + } + + for (EditorState editorState : editorList) { + editorState.applyNonDefaults(saveTo); + } + return saveTo; + } + + private void loadCurrentOptionsIntoEditors() { + List analyzerStates = model.getModelData(); + for (AnalyzerEnablementState analyzerState : analyzerStates) { + String analyzerName = analyzerState.getName(); + Object defaultObject = analysisOptions.getDefaultValue(analyzerName); + boolean defaultValue = + (defaultObject instanceof Boolean) ? (Boolean) defaultObject : false; + boolean newValue = selectedOptions.getBoolean(analyzerName, defaultValue); + analyzerState.setEnabled(newValue); + setAnalyzerEnabled(analyzerName, newValue, false); + model.fireTableRowsUpdated(0, model.getRowCount() - 1); + } + + for (EditorState editorState : editorList) { + editorState.loadFrom(selectedOptions); + } + updateDeleteButton(); + } + + private void reloadOptionsCombo(Options newDefaultOptions) { + Options[] defaultOptionsArray = getDefaultOptionsArray(); + defaultOptionsCombo.setModel(new DefaultComboBoxModel(defaultOptionsArray)); + Options selected = findOptions(defaultOptionsArray, newDefaultOptions.getName()); + defaultOptionsCombo.setSelectedItem(selected); + } + + private Options findOptions(Options[] defaultOptionsArray, String name) { + for (Options fileOptions : defaultOptionsArray) { + if (fileOptions.getName().equals(name)) { + return fileOptions; + } + } + return STANDARD_DEFAULT_OPTIONS; } private void configureEnabledColumnWidth(int width) { @@ -290,7 +452,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { component.setBorder(compoundBorder); } - void setAnalyzerEnabled(String analyzerName, boolean enabled) { + void setAnalyzerEnabled(String analyzerName, boolean enabled, boolean fireEvent) { List list = analyzerManagedComponentsMap.get(analyzerName); if (list != null) { Iterator iterator = list.iterator(); @@ -299,18 +461,20 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { next.setEnabled(enabled); } } - propertyChange(null); + if (fireEvent) { + propertyChange(null); + } } @Override public void propertyChange(PropertyChangeEvent evt) { - if (checkForDifferences()) { + if (checkForDifferencesWithProgram()) { propertyChangeListener.propertyChange( new PropertyChangeEvent(this, GhidraOptions.APPLY_ENABLED, null, Boolean.TRUE)); } } - private boolean checkForDifferences() { + private boolean checkForDifferencesWithProgram() { List analyzerStates = model.getModelData(); boolean changes = false; for (int i = 0; i < analyzerStates.size(); ++i) { @@ -342,26 +506,56 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { * analyzed. */ void applyChanges() { - List analyzerStates = model.getModelData(); - for (AnalyzerEnablementState analyzerState : analyzerStates) { - String analyzerName = analyzerState.getName(); - boolean enabled = analyzerState.isEnabled(); - - int id = programs.get(0).startTransaction("setting analysis options"); - boolean commit = false; - try { + int id = programs.get(0).startTransaction("setting analysis options"); + boolean commit = false; + try { + List analyzerStates = model.getModelData(); + for (AnalyzerEnablementState analyzerState : analyzerStates) { + String analyzerName = analyzerState.getName(); + boolean enabled = analyzerState.isEnabled(); analysisOptions.setBoolean(analyzerName, enabled); commit = true; } - finally { - programs.get(0).endTransaction(id, commit); + for (EditorState info : editorList) { + info.applyValue(); } - - updateOptionForAllPrograms(analyzerName, enabled); + } + finally { + programs.get(0).endTransaction(id, commit); } - for (EditorState info : editorList) { - info.applyValue(); + copyOptionsToAllPrograms(); + currentProgramOptions = getNonDefaultProgramOptions(); + reloadOptionsCombo(currentProgramOptions); + } + + private void copyOptionsToAllPrograms() { + for (int i = 1; i < programs.size(); i++) { + Program program = programs.get(i); + + int id = program.startTransaction("setting analysis options"); + boolean commit = false; + try { + copyOptionsTo(program); + commit = true; + } + finally { + program.endTransaction(id, commit); + } + } + } + + private void copyOptionsTo(Program program) { + Options destinationOptions = program.getOptions(Program.ANALYSIS_PROPERTIES); + + // first remove all options in destination + for (String optionName : destinationOptions.getOptionNames()) { + destinationOptions.removeOption(optionName); + } + + // now copy all the options in the source + for (String optionName : analysisOptions.getOptionNames()) { + destinationOptions.putObject(optionName, analysisOptions.getObject(optionName, null)); } } @@ -415,7 +609,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { if (value instanceof Boolean) { enabled = (Boolean) value; } - setAnalyzerEnabled(analyzerName, enabled); + setAnalyzerEnabled(analyzerName, enabled, false); } } @@ -478,4 +672,89 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { } } + private String getLastUsedDefaultOptionsName() { + // if the program has non-default options, then it has been analyzed before, use its + // current settings initially + if (isAnalyzed()) { + return currentProgramOptions.getName(); + } + // Otherwise, use the last used analysis options configuration + return Preferences.getProperty(LAST_DEFAULT_OPTIONS, STANDARD_DEFAULT_OPTIONS.getName()); + } + + private boolean isAnalyzed() { + Options options = programs.get(0).getOptions(Program.PROGRAM_INFO); + return options.getBoolean(Program.ANALYZED, false); + } + + private Options[] getDefaultOptionsArray() { + List savedDefaultsList = getSavedOptionsObjects(); + Options[] optionsArray = new FileOptions[savedDefaultsList.size() + 2]; // 2 standard configurations always present + optionsArray[0] = currentProgramOptions; + optionsArray[1] = STANDARD_DEFAULT_OPTIONS; + for (int i = 0; i < savedDefaultsList.size(); i++) { + optionsArray[i + 2] = savedDefaultsList.get(i); + } + return optionsArray; + } + + private File getOptionsSaveFile(String saveName) { + File userSettingsDirectory = Application.getUserSettingsDirectory(); + File optionsDir = new File(userSettingsDirectory, ANALYZER_OPTIONS_SAVE_DIR); + FileUtilities.mkdirs(optionsDir); + return new File(optionsDir, saveName + "." + OPTIONS_FILE_EXTENSION); + } + + private List getSavedOptionsObjects() { + File userSettingsDirectory = Application.getUserSettingsDirectory(); + File optionsDir = new File(userSettingsDirectory, ANALYZER_OPTIONS_SAVE_DIR); + if (!optionsDir.isDirectory()) { + return Collections.emptyList(); + } + List list = new ArrayList<>(); + File[] listFiles = optionsDir.listFiles(); + for (File file : listFiles) { + if (OPTIONS_FILE_EXTENSION.equals(FilenameUtils.getExtension(file.getName()))) { + FileOptions fileOptions; + try { + fileOptions = new FileOptions(file); + list.add(fileOptions); + } + catch (IOException e) { + Msg.error(this, "Error reading saved analysis options", e); + } + } + } + + return list; + } + + private boolean isUserConfiguration(Options options) { + if (options == STANDARD_DEFAULT_OPTIONS || + options == currentProgramOptions) { + // these two are not user configurations. + return false; + } + return true; + + } + + @Override + public void itemStateChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + selectedOptions = (FileOptions) defaultOptionsCombo.getSelectedItem(); + updateDeleteButton(); + loadCurrentOptionsIntoEditors(); + // save off preference (unless it is the current program options, then don't save it) + if (selectedOptions != currentProgramOptions) { + Preferences.setProperty(LAST_DEFAULT_OPTIONS, selectedOptions.getName()); + } + } + } + + private void updateDeleteButton() { + if (deleteButton != null) { + deleteButton.setEnabled(isUserConfiguration(selectedOptions)); + } + } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java index 003cfa959b..23706714a8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java @@ -1111,7 +1111,7 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl "" + HTMLUtilities.escapeHTML(program.getDomainFile().getName()) + " has not been analyzed. Would you like to analyze it now?"); boolean analyzed = answer == OptionDialog.OPTION_ONE; - GhidraProgramUtilities.setAnalyzedFlag(program, analyzed); + GhidraProgramUtilities.setAnalyzedFlag(program, false); return analyzed; } return false; diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalyzeAllOpenProgramsTaskTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalyzeAllOpenProgramsTaskTest.java index 8ba6671298..204d2fbfd7 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalyzeAllOpenProgramsTaskTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalyzeAllOpenProgramsTaskTest.java @@ -297,6 +297,7 @@ public class AnalyzeAllOpenProgramsTaskTest extends AbstractGhidraHeadedIntegrat AnalysisPanel panel = findComponent(optionsDialog.getComponent(), AnalysisPanel.class, false); invokeInstanceMethod("deselectAll", panel); + panel.applyChanges(); waitForSwing(); close(optionsDialog); diff --git a/Ghidra/Features/Base/src/test/java/ghidra/framework/options/FileOptionsTest.java b/Ghidra/Features/Base/src/test/java/ghidra/framework/options/FileOptionsTest.java new file mode 100644 index 0000000000..4015093c27 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/framework/options/FileOptionsTest.java @@ -0,0 +1,132 @@ +/* ### + * 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.framework.options; + +import static org.junit.Assert.*; + +import java.awt.Color; +import java.io.File; +import java.io.IOException; + +import org.junit.Test; + +import generic.test.AbstractGenericTest; + +public class FileOptionsTest extends AbstractGenericTest { + + @Test + public void testSavingRestoringFileOptions() throws IOException { + FileOptions options = new FileOptions("Test"); + + options.registerOption("aaa", Integer.valueOf(5), null, "aaa description"); + options.registerOption("bbb", Integer.valueOf(5), null, "bbb description"); + options.registerOption("ccc", Color.RED, null, "ccc description"); + + TestCustomOption custom = new TestCustomOption("bob", 23, true); + + options.setInt("aaa", 10); + options.setColor("ccc", Color.BLUE); + options.setCustomOption("ddd", custom); + + assertEquals(10, options.getInt("aaa", 0)); + assertEquals(5, options.getInt("bbb", 0)); + assertEquals(Color.BLUE, options.getColor("ccc", null)); + assertEquals(custom, options.getCustomOption("ddd", null)); + + File file = createTempFile("optionsFile", "options"); + + options.save(file); + + FileOptions restored = new FileOptions(file); + + assertEquals(10, restored.getInt("aaa", 0)); + assertFalse(restored.contains("bbb")); // default value should not have been saved + assertEquals(Color.BLUE, restored.getColor("ccc", null)); + assertEquals(custom, restored.getCustomOption("ddd", null)); + } + + public static class TestCustomOption implements CustomOption { + + String name; + int count; + boolean active; + + public TestCustomOption() { + + } + + public TestCustomOption(String name, int count, boolean active) { + this.name = name; + this.count = count; + this.active = active; + } + + @Override + public void readState(SaveState saveState) { + name = saveState.getString("name", null); + count = saveState.getInt("count", 0); + active = saveState.getBoolean("active", false); + } + + @Override + public void writeState(SaveState saveState) { + saveState.putString("name", name); + saveState.putInt("count", count); + saveState.putBoolean("active", active); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (active ? 1231 : 1237); + result = prime * result + count; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TestCustomOption other = (TestCustomOption) obj; + if (active != other.active) { + return false; + } + if (count != other.count) { + return false; + } + if (name == null) { + if (other.name != null) { + return false; + } + } + else if (!name.equals(other.name)) { + return false; + } + return true; + } + + } + +} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java index aa115f42cb..644da95148 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorState.java @@ -17,11 +17,9 @@ package ghidra.framework.options; import java.awt.Component; import java.beans.*; -import java.util.HashSet; -import java.util.Set; +import java.util.*; import ghidra.framework.Application; -import ghidra.util.SystemUtilities; public class EditorState implements PropertyChangeListener { @@ -90,11 +88,32 @@ public class EditorState implements PropertyChangeListener { } public boolean isValueChanged() { - return !SystemUtilities.isEqual(currentValue, originalValue); + return !Objects.equals(currentValue, originalValue); + } + + public void applyNonDefaults(Options save) { + if (!Objects.equals(currentValue, options.getDefaultValue(name))) { + Options sub = save.getOptions(options.getName()); + sub.putObject(name, currentValue); + } + } + + public void loadFrom(Options loadFrom) { + Options sub = loadFrom.getOptions(options.getName()); + Object newValue = sub.getObject(name, options.getDefaultValue(name)); + if (editor != null && !Objects.equals(currentValue, newValue)) { + editor.setValue(newValue); + } + } + + public boolean hasSameValue(Options compareTo) { + Options sub = compareTo.getOptions(options.getName()); + Object newValue = sub.getObject(name, options.getDefaultValue(name)); + return Objects.equals(newValue, currentValue); } public void applyValue() { - if (SystemUtilities.isEqual(currentValue, originalValue)) { + if (Objects.equals(currentValue, originalValue)) { return; } boolean success = false; @@ -166,4 +185,5 @@ public class EditorState implements PropertyChangeListener { public String getDescription() { return options.getDescription(name); } + } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorStateFactory.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorStateFactory.java index d494e01f49..742910fd10 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorStateFactory.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/EditorStateFactory.java @@ -1,6 +1,5 @@ /* ### * 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. @@ -26,7 +25,8 @@ public class EditorStateFactory { public EditorStateFactory() { } - public EditorState getEditorState(Options options, String name, PropertyChangeListener listener) { + public EditorState getEditorState(Options options, String name, + PropertyChangeListener listener) { String optionID = options.getID(name); EditorState editorState = cache.get(optionID); diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/FileOptions.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/FileOptions.java new file mode 100644 index 0000000000..29da331428 --- /dev/null +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/FileOptions.java @@ -0,0 +1,156 @@ +/* ### + * 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.framework.options; + +import java.beans.PropertyEditor; +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FilenameUtils; + +import ghidra.util.HelpLocation; +import ghidra.util.Msg; + +public class FileOptions extends AbstractOptions { + + private File file; + + public FileOptions(String name) { + super(name); + } + + public FileOptions(File file) throws IOException { + this(FilenameUtils.getBaseName(file.getName())); + this.file = file; + loadFromFile(); + } + + public void save(File saveFile) throws IOException { + this.name = FilenameUtils.getBaseName(saveFile.getName()); + this.file = saveFile; + saveToFile(); + } + + public File getFile() { + return file; + } + + public CustomOption readCustomOption(SaveState saveState) { + String customOptionClassName = saveState.getString("CUSTOM_OPTION_CLASS", null); + try { + Class c = Class.forName(customOptionClassName); + CustomOption customOption = (CustomOption) c.getDeclaredConstructor().newInstance(); + customOption.readState(saveState); + return customOption; + } + catch (Exception e) { + Msg.error(this, "Can't create customOption instance for: " + customOptionClassName, e); + } + return null; + } + + private void loadFromFile() throws IOException { + SaveState saveState = SaveState.readJsonFile(file); + for (String optionName : saveState.getNames()) { + Object object = saveState.getObject(optionName); + if (object instanceof SaveState) { + SaveState customState = (SaveState) object; + object = readCustomOption(customState); + } + Option option = + createUnregisteredOption(optionName, OptionType.getOptionType(object), null); + option.doSetCurrentValue(object); // use doSet versus set so that it is not registered + valueMap.put(optionName, option); + } + } + + private void saveToFile() throws IOException { + SaveState saveState = new SaveState("File_Options"); + + for (String optionName : valueMap.keySet()) { + Option optionValue = valueMap.get(optionName); + if (!optionValue.isDefault()) { + Object value = optionValue.getValue(null); + if (value instanceof CustomOption) { + SaveState customState = new SaveState(); + customState.putString("CUSTOM_OPTION_CLASS", value.getClass().getName()); + ((CustomOption) value).writeState(customState); + value = customState; + } + saveState.putObject(optionName, value); + } + } + saveState.saveToJsonFile(file); + } + + @Override + protected Option createRegisteredOption(String optionName, OptionType type, String description, + HelpLocation help, Object defaultValue, PropertyEditor editor) { + return new FileOption(optionName, type, description, help, defaultValue, true, editor); + } + + @Override + protected Option createUnregisteredOption(String optionName, OptionType type, + Object defaultValue) { + return new FileOption(optionName, type, null, null, defaultValue, false, null); + } + + @Override + protected boolean notifyOptionChanged(String optionName, Object oldValue, Object newValue) { + // do nothing for now + return true; + } + + private static class FileOption extends Option { + private Object currentValue; + + FileOption(String name, OptionType type, String description, HelpLocation helpLocation, + Object defaultValue, boolean isRegistered, PropertyEditor editor) { + super(name, type, description, helpLocation, defaultValue, isRegistered, editor); + + this.currentValue = defaultValue; + } + + @Override + public Object getCurrentValue() { + return currentValue; + } + + @Override + public void doSetCurrentValue(Object value) { + currentValue = value; + } + } + + @Override + public String toString() { + return name; + } + + public FileOptions copy() { + FileOptions copy = new FileOptions("new"); + + for (String optionName : valueMap.keySet()) { + Option optionValue = valueMap.get(optionName); + if (!optionValue.isDefault()) { + Object value = optionValue.getValue(null); + copy.putObject(optionName, value); + } + } + + return copy; + } +} From 515abf8596f58c9f436fa018e377cf88f9bc2753 Mon Sep 17 00:00:00 2001 From: ghidravore Date: Thu, 31 Dec 2020 08:00:13 -0500 Subject: [PATCH 2/4] added comment as to why propertyChange no longer does anything --- .../core/analysis/AnalysisOptionsDialog.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java index 9098a00519..4b215963ba 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java @@ -66,17 +66,10 @@ public class AnalysisOptionsDialog extends DialogComponentProvider implements @Override public void propertyChange(PropertyChangeEvent evt) { - -// // On any analyzer status change, update the options for all programs -// // being analyzed. This is necessary to keep options consistent across all -// // programs being analyzed. -// // -// // Note: It's possible to receive a property change notification before the -// // analysis panel has finished being constructed, so protect against -// // that before calling the update method. -// if (panel != null) { -// panel.updateOptionForAllPrograms(evt.getPropertyName(), (Boolean) evt.getNewValue()); -// } + // This used to update all the programs every time the user made a change in the gui. This + // was bad. Instead, the "applyChanges" now saves the options to the first program and then copies + // them all to each of the other program. This way, if the user cancels the dialog, none + // of the program's options are changed. } @Override From fc1bf3b839794d4f2f52e099370675b3aa5a557d Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Fri, 8 Jan 2021 16:03:55 -0500 Subject: [PATCH 3/4] GP-544 Revisions for analysis options per review comments --- .../AutoAnalysisPlugin/AutoAnalysis.htm | 27 ++ .../core/analysis/AnalysisOptionsDialog.java | 14 +- .../plugin/core/analysis/AnalysisPanel.java | 78 +++- .../core/analysis/AnalysisOptionsTest.java | 355 ++++++++++++++++++ 4 files changed, 448 insertions(+), 26 deletions(-) create mode 100644 Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptionsTest.java diff --git a/Ghidra/Features/Base/src/main/help/help/topics/AutoAnalysisPlugin/AutoAnalysis.htm b/Ghidra/Features/Base/src/main/help/help/topics/AutoAnalysisPlugin/AutoAnalysis.htm index d9799704d8..db9545857b 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/AutoAnalysisPlugin/AutoAnalysis.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/AutoAnalysisPlugin/AutoAnalysis.htm @@ -151,12 +151,38 @@

A separate dialog showing the Analysis options is also displayed after a new program has been imported, or the Analysis Auto Analysis menu item is chosen.

+ +

Some analyzers only work on certain processors. For example, the MIPS Instruction Analyzer only works on MIPS processors. Their options will only show up if the currently open program can be analyzed with the analyzer.

+ +

Saved Options Configurations

+
+

The Options Configurations combo box at bottom of the analyzer enablement panel can be used to quickly + switch between sets of analyzer option values. The combo box will always contain two standard sets of options as well as any sets of + any previously saved configurations. The two standard configurations are: +

    +
  • Standard Defaults - This setting will put all the analyzer enablements and options to the original default values.
  • +
  • Current Program Options - This setting will put all the analyzer enablements and options to values as stored in + the current program.
  • +
+
+ +

Analysis Panel Buttons

+
+
    +
  • Select All - Turns all analyzers on.
  • +
  • Deselect All - Turns all analyzers off.
  • +
  • Reset - Resets all the options to the currently selected configuration.
  • +
  • Save - Saves the current settings to a named configuration.
  • +
  • Delete - Deletes the currently selected named configuration.
  • +
+
+

Auto Analyzers

@@ -613,3 +639,4 @@
+ \ No newline at end of file diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java index 4b215963ba..e90b32f242 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisOptionsDialog.java @@ -29,8 +29,7 @@ import ghidra.util.Msg; * Dialog to show the panel for the auto analysis options. * */ -public class AnalysisOptionsDialog extends DialogComponentProvider implements - PropertyChangeListener { +public class AnalysisOptionsDialog extends DialogComponentProvider { private boolean doAnalysis; private AnalysisPanel panel; private EditorStateFactory editorStateFactory = new EditorStateFactory(); @@ -64,14 +63,6 @@ public class AnalysisOptionsDialog extends DialogComponentProvider implements setRememberSize(true); } - @Override - public void propertyChange(PropertyChangeEvent evt) { - // This used to update all the programs every time the user made a change in the gui. This - // was bad. Instead, the "applyChanges" now saves the options to the first program and then copies - // them all to each of the other program. This way, if the user cancels the dialog, none - // of the program's options are changed. - } - @Override public void okCallback() { try { @@ -95,7 +86,8 @@ public class AnalysisOptionsDialog extends DialogComponentProvider implements * @return the new analysis panel */ private AnalysisPanel buildComponent(List programs) { - AnalysisPanel panel = new AnalysisPanel(programs, editorStateFactory, this); + AnalysisPanel panel = new AnalysisPanel(programs, editorStateFactory); return panel; } } + diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java index fa230cf3e9..e023527bcd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java @@ -42,6 +42,7 @@ import docking.widgets.table.GTable; import ghidra.GhidraOptions; import ghidra.app.services.Analyzer; import ghidra.framework.Application; +import ghidra.framework.GenericRunInfo; import ghidra.framework.options.*; import ghidra.framework.preferences.Preferences; import ghidra.program.model.listing.Program; @@ -51,7 +52,7 @@ import ghidra.util.exception.AssertException; import ghidra.util.layout.VerticalLayout; import utilities.util.FileUtilities; -class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListener { +class AnalysisPanel extends JPanel implements PropertyChangeListener { // create an empty options to represent the defaults of the analyzers private static final Options STANDARD_DEFAULT_OPTIONS = new FileOptions("Standard Defaults"); @@ -60,8 +61,8 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen public static final String PROTOTYPE = " (Prototype)"; public final static int COLUMN_ANALYZER_IS_ENABLED = 0; - private static final String ANALYZER_OPTIONS_SAVE_DIR = "AnalyzerDefaults"; - private static final String LAST_DEFAULT_OPTIONS = "LAST_OPTIONS_DEFAULTS"; + private static final String ANALYZER_OPTIONS_SAVE_DIR = "analyzer_options"; + private static final String LAST_DEFAULT_OPTIONS = "DEFAULT_ANALYSIS_OPTIONS"; private List programs; private PropertyChangeListener propertyChangeListener; @@ -95,6 +96,18 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen this(List.of(program), editorStateFactory, propertyChangeListener); } + /** + * Constructor + * + * @param programs list of programs that will be analyzed + * @param editorStateFactory the editor factory + * @param propertyChangeListener subscriber for property change notifications + */ + AnalysisPanel(List programs, EditorStateFactory editorStateFactory) { + this(programs, editorStateFactory, e -> {}); + } + + /** * Constructor * @@ -180,8 +193,6 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen setLayout(new BorderLayout()); setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); add(buildMainPanel(), BorderLayout.CENTER); -// add(buildOptionsComboBoxPanel(), BorderLayout.NORTH); -// add(buildMainPanel(), BorderLayout.CENTER); } private JComponent buildMainPanel() { @@ -207,7 +218,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen defaultOptionsCombo = new GhidraComboBox<>(defaultOptionsArray); selectedOptions = findOptions(defaultOptionsArray, getLastUsedDefaultOptionsName()); defaultOptionsCombo.setSelectedItem(selectedOptions); - defaultOptionsCombo.addItemListener(this); + defaultOptionsCombo.addItemListener(this::analysisComboChanged); Dimension preferredSize = defaultOptionsCombo.getPreferredSize(); defaultOptionsCombo.setPreferredSize(new Dimension(200, preferredSize.height)); panel.add(defaultOptionsCombo); @@ -343,7 +354,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen reloadOptionsCombo(saved); } catch (IOException e) { - Msg.error(this, "Error Saving Default Options", e); + Msg.error(this, "Error saving default options", e); } } @@ -506,7 +517,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen * analyzed. */ void applyChanges() { - int id = programs.get(0).startTransaction("setting analysis options"); + int id = programs.get(0).startTransaction("Setting Analysis Options"); boolean commit = false; try { List analyzerStates = model.getModelData(); @@ -533,7 +544,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen for (int i = 1; i < programs.size(); i++) { Program program = programs.get(i); - int id = program.startTransaction("setting analysis options"); + int id = program.startTransaction("Setting Analysis Options"); boolean commit = false; try { copyOptionsTo(program); @@ -709,8 +720,13 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen File userSettingsDirectory = Application.getUserSettingsDirectory(); File optionsDir = new File(userSettingsDirectory, ANALYZER_OPTIONS_SAVE_DIR); if (!optionsDir.isDirectory()) { - return Collections.emptyList(); + // new installation, copy any old saved analysis options files to current + migrateOptionsFromPreviousRevision(optionsDir); } + return readSavedOptions(optionsDir); + } + + private List readSavedOptions(File optionsDir) { List list = new ArrayList<>(); File[] listFiles = optionsDir.listFiles(); for (File file : listFiles) { @@ -729,6 +745,40 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen return list; } + private void migrateOptionsFromPreviousRevision(File optionsDir) { + FileUtilities.mkdirs(optionsDir); + File previous = getMostRecentApplicationSettingsDirWithSavedOptions(); + if (previous == null) { + return; + } + List readSavedOptions = readSavedOptions(previous); + for (Options options : readSavedOptions) { + FileOptions fileOptions = (FileOptions) options; + String name = fileOptions.getName(); + try { + fileOptions.save(getOptionsSaveFile(name)); + } catch (IOException e) { + Msg.error(this, "Error copying analysis options from previous Ghidra install", e); + } + } + } + private File getMostRecentApplicationSettingsDirWithSavedOptions() { + List ghidraUserDirsByTime = GenericRunInfo.getPreviousApplicationSettingsDirsByTime(); + if (ghidraUserDirsByTime.size() == 0) { + return null; + } + + // get the tools from the most recent projects first + for (File ghidraUserDir : ghidraUserDirsByTime) { + File possible = new File(ghidraUserDir, ANALYZER_OPTIONS_SAVE_DIR); + if (possible.exists()) { + return possible; + } + } + return null; + } + + private boolean isUserConfiguration(Options options) { if (options == STANDARD_DEFAULT_OPTIONS || options == currentProgramOptions) { @@ -739,8 +789,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen } - @Override - public void itemStateChanged(ItemEvent e) { + private void analysisComboChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { selectedOptions = (FileOptions) defaultOptionsCombo.getSelectedItem(); updateDeleteButton(); @@ -753,8 +802,7 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener, ItemListen } private void updateDeleteButton() { - if (deleteButton != null) { - deleteButton.setEnabled(isUserConfiguration(selectedOptions)); - } + deleteButton.setEnabled(isUserConfiguration(selectedOptions)); } } + diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptionsTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptionsTest.java new file mode 100644 index 0000000000..e08a257ff3 --- /dev/null +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/analysis/AnalysisOptionsTest.java @@ -0,0 +1,355 @@ +/* ### + * 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.app.plugin.core.analysis; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.swing.ComboBoxModel; +import javax.swing.table.TableModel; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import docking.action.DockingActionIf; +import docking.widgets.OptionDialog; +import docking.widgets.combobox.GhidraComboBox; +import docking.widgets.dialogs.InputDialog; +import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; +import ghidra.app.services.ProgramManager; +import ghidra.framework.options.Options; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.database.ProgramBuilder; +import ghidra.program.model.listing.Program; +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.test.TestEnv; + +public class AnalysisOptionsTest extends AbstractGhidraHeadedIntegrationTest { + + private TestEnv env; + private PluginTool tool; + private Program program; + private AnalysisOptionsDialog optionsDialog; + + @Before + public void setUp() throws Exception { + env = new TestEnv(); + tool = env.getTool(); + tool.addPlugin(CodeBrowserPlugin.class.getName()); + addPlugin(tool, AutoAnalysisPlugin.class); + showTool(tool); + program = buildProgram("test", ProgramBuilder._TOY); + ProgramManager pm = tool.getService(ProgramManager.class); + pm.openProgram(program.getDomainFile()); + optionsDialog = invokeAnalysisDialog(); + } + + private Program buildProgram(String name, String languageID) throws Exception { + ProgramBuilder builder = new ProgramBuilder(name, languageID); + builder.createMemory("test1", "0x1000", 0x2000); + return builder.getProgram(); + } + + @After + public void tearDown() throws Exception { + env.release(program); + tool.close(); + env.dispose(); + } + + @Test + public void testSelectAll() throws Exception { + + setAnalyzerEnabled("Stack", false); + setAnalyzerEnabled("Reference", false); + setAnalyzerEnabled("ASCII Strings", false); + + assertFalse(isAnalyzerEnabled("Stack")); + assertFalse(isAnalyzerEnabled("Reference")); + assertFalse(isAnalyzerEnabled("ASCII Strings")); + + pressButtonByText(optionsDialog, "Select All"); + + assertTrue(isAnalyzerEnabled("Stack")); + assertTrue(isAnalyzerEnabled("Reference")); + assertTrue(isAnalyzerEnabled("ASCII Strings")); + } + @Test + public void testDeselectAll() throws Exception { + + setAnalyzerEnabled("Stack", true); + setAnalyzerEnabled("Reference", true); + setAnalyzerEnabled("ASCII Strings",true); + + assertTrue(isAnalyzerEnabled("Stack")); + assertTrue(isAnalyzerEnabled("Reference")); + assertTrue(isAnalyzerEnabled("ASCII Strings")); + + pressButtonByText(optionsDialog, "Deselect All"); + + assertFalse(isAnalyzerEnabled("Stack")); + assertFalse(isAnalyzerEnabled("Reference")); + assertFalse(isAnalyzerEnabled("ASCII Strings")); + } + + @Test + public void testReset() throws Exception { + assertTrue(isAnalyzerEnabled("Stack")); + assertTrue(isAnalyzerEnabled("Reference")); + assertTrue(isAnalyzerEnabled("ASCII Strings")); + + setAnalyzerEnabled("Stack", false); + setAnalyzerEnabled("Reference", false); + setAnalyzerEnabled("ASCII Strings", false); + + assertFalse(isAnalyzerEnabled("Stack")); + assertFalse(isAnalyzerEnabled("Reference")); + assertFalse(isAnalyzerEnabled("ASCII Strings")); + + pressButtonByText(optionsDialog, "Reset"); + + assertTrue(isAnalyzerEnabled("Stack")); + assertTrue(isAnalyzerEnabled("Reference")); + assertTrue(isAnalyzerEnabled("ASCII Strings")); + } + @Test + public void testSaveConfiguration() { + assertComboboxEquals("Current Program Options"); + setAnalyzerEnabled("Stack", false); + setAnalyzerEnabled("Reference", false); + setAnalyzerEnabled("ASCII Strings", false); + + pressButtonByText(optionsDialog, "Save", false); + saveConfig("foo"); + + assertComboboxEquals("foo"); + + } + @Test + public void testDeleteConfiguration() { + assertComboboxEquals("Current Program Options"); + createConfig("foo", false, false, false); + assertComboboxEquals("foo"); + + pressButtonByText(optionsDialog, "Delete", false); + confirmDelete(); + + assertComboboxEquals("Current Program Options"); + } + + @Test + public void testSwitchCombo() { + createConfig("a", false, false, false); + createConfig("b", false, true, false); + createConfig("c", true, false, true); + + assertComboboxEquals("c"); + + assertTrue(isAnalyzerEnabled("Stack")); + assertFalse(isAnalyzerEnabled("Reference")); + assertTrue(isAnalyzerEnabled("ASCII Strings")); + + setCombobox("a"); + assertFalse(isAnalyzerEnabled("Stack")); + assertFalse(isAnalyzerEnabled("Reference")); + assertFalse(isAnalyzerEnabled("ASCII Strings")); + + setCombobox("b"); + assertFalse(isAnalyzerEnabled("Stack")); + assertTrue(isAnalyzerEnabled("Reference")); + assertFalse(isAnalyzerEnabled("ASCII Strings")); + + } + + @Test + public void testCancelDialogDoesntSaveChanges() { + assertComboboxEquals("Current Program Options"); + + assertTrue(isAnalyzerEnabledInProgramOptions("Stack")); + assertTrue(isAnalyzerEnabledInProgramOptions("Reference")); + assertTrue(isAnalyzerEnabled("Stack")); + assertTrue(isAnalyzerEnabled("Reference")); + + setAnalyzerEnabled("Stack", false); + setAnalyzerEnabled("Reference", false); + + assertTrue(isAnalyzerEnabledInProgramOptions("Stack")); + assertTrue(isAnalyzerEnabledInProgramOptions("Reference")); + assertFalse(isAnalyzerEnabled("Stack")); + assertFalse(isAnalyzerEnabled("Reference")); + + pressButtonByText(optionsDialog, "Cancel"); + + assertTrue(isAnalyzerEnabledInProgramOptions("Stack")); + assertTrue(isAnalyzerEnabledInProgramOptions("Reference")); + } + + @Test + public void testAnalyzeSavesChangesToProgram() { + assertComboboxEquals("Current Program Options"); + + assertTrue(isAnalyzerEnabledInProgramOptions("Stack")); + assertTrue(isAnalyzerEnabledInProgramOptions("Reference")); + assertTrue(isAnalyzerEnabled("Stack")); + assertTrue(isAnalyzerEnabled("Reference")); + + setAnalyzerEnabled("Stack", false); + setAnalyzerEnabled("Reference", false); + + assertTrue(isAnalyzerEnabledInProgramOptions("Stack")); + assertTrue(isAnalyzerEnabledInProgramOptions("Reference")); + assertFalse(isAnalyzerEnabled("Stack")); + assertFalse(isAnalyzerEnabled("Reference")); + + pressButtonByText(optionsDialog, "Analyze"); + + assertFalse(isAnalyzerEnabledInProgramOptions("Stack")); + assertFalse(isAnalyzerEnabledInProgramOptions("Reference")); + } + +//================================================================================================== +// Private Methods +//================================================================================================== + private void createConfig(String name, boolean stackOn, boolean refOn, boolean stringOn) { + setAnalyzerEnabled("Stack", stackOn); + setAnalyzerEnabled("Reference", refOn); + setAnalyzerEnabled("ASCII Strings", stringOn); + + pressButtonByText(optionsDialog, "Save", false); + saveConfig(name); + + } + + private void assertComboboxEquals(String name) { + AnalysisPanel panel = (AnalysisPanel) getInstanceField("panel", optionsDialog); + @SuppressWarnings("unchecked") + GhidraComboBox combo = (GhidraComboBox) getInstanceField("defaultOptionsCombo", panel); + assertEquals(name, ((Options)combo.getSelectedItem()).getName()); + } + + private void setCombobox(String name) { + runSwing(() -> { + AnalysisPanel panel = (AnalysisPanel) getInstanceField("panel", optionsDialog); + @SuppressWarnings("unchecked") + GhidraComboBox combo = (GhidraComboBox) getInstanceField("defaultOptionsCombo", panel); + ComboBoxModel model = combo.getModel(); + for (int i = 0; i < model.getSize(); i++) { + Options elementAt = model.getElementAt(i); + if (elementAt.getName().equals(name)) { + combo.setSelectedItem(elementAt); + return; + } + } + fail("Couldn't find combobox item: " + name); + }); + } + + private void saveConfig(String name) { + InputDialog dialog = waitForDialogComponent(InputDialog.class); + runSwing(() -> dialog.setValue(name)); + pressButtonByText(dialog, "OK"); + waitForSwing(); + + } + private void confirmDelete() { + OptionDialog dialog = waitForDialogComponent(OptionDialog.class); + pressButtonByText(dialog, "Yes"); + waitForSwing(); + } + + private AnalysisOptionsDialog invokeAnalysisDialog() { + DockingActionIf action = getAction(tool, "Auto Analyze"); + performAction(action, false); + return waitForDialogComponent(AnalysisOptionsDialog.class); + } + + private void apply() { + pressButtonByText(optionsDialog, "Analyze"); + } + + + private boolean isAnalyzerEnabledInProgramOptions(String analyzerName) { + Options options = program.getOptions(Program.ANALYSIS_PROPERTIES); + return options.getBoolean(analyzerName, false); + } + + private void verifyDefaultOptions(Collection programs) { + for (Program program : programs) { + Options options = program.getOptions(Program.ANALYSIS_PROPERTIES); + for (String name : options.getOptionNames()) { + assertTrue("Program options are unexpectedly changed: " + program, + options.isDefaultValue(name)); + } + } + } + + private void setAnalyzerEnabled(String name, boolean enabled) { + TableModel model = getAnalyzerTableModel(); + int analyzerRow = getRowForAnalyzer(name, model); + runSwing(new Runnable() { + @Override + public void run() { + model.setValueAt(enabled, analyzerRow, 0); + } + }); + } + + private boolean isAnalyzerEnabled(String name) { + TableModel model = getAnalyzerTableModel(); + int analyzerRow = getRowForAnalyzer(name, model); + AtomicBoolean result = new AtomicBoolean(); + runSwing(new Runnable() { + @Override + public void run() { + result.set((Boolean)model.getValueAt(analyzerRow, 0)); + } + }); + return result.get(); + } + + private TableModel getAnalyzerTableModel() { + // The analysis dialog uses a table to display the enablement and name of each + // analyzer + AnalysisPanel panel = (AnalysisPanel) getInstanceField("panel", optionsDialog); + return (TableModel) getInstanceField("model", panel); + } + + private int getRowForAnalyzer(String name, TableModel model) { + int rowCount = model.getRowCount(); + int row = 0; + for (row = 0; row < rowCount; row++) { + String rowName = (String) model.getValueAt(row, 1); + if (name.equals(rowName)) { + break;// found it + } + } + + if (row == rowCount) { + Assert.fail("Couldn't find analyzer named " + name); + } + int analyzerRow = row; + return analyzerRow; + } + +} + From eb5a9d1f203cdcbdbbefca498e5ce3cc322078fb Mon Sep 17 00:00:00 2001 From: ghidra1 Date: Fri, 8 Jan 2021 16:15:41 -0500 Subject: [PATCH 4/4] GP-544 minor preference name change --- .../app/plugin/core/analysis/AnalysisPanel.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java index e023527bcd..ae0671d770 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AnalysisPanel.java @@ -17,7 +17,6 @@ package ghidra.app.plugin.core.analysis; import java.awt.*; import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; import java.beans.*; import java.io.File; import java.io.IOException; @@ -62,7 +61,9 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { public final static int COLUMN_ANALYZER_IS_ENABLED = 0; private static final String ANALYZER_OPTIONS_SAVE_DIR = "analyzer_options"; - private static final String LAST_DEFAULT_OPTIONS = "DEFAULT_ANALYSIS_OPTIONS"; + + // preference which retains last used analyzer_options file name + private static final String LAST_DEFAULT_OPTIONS = "LAST_ANALYSIS_OPTIONS_USED"; private List programs; private PropertyChangeListener propertyChangeListener; @@ -488,9 +489,9 @@ class AnalysisPanel extends JPanel implements PropertyChangeListener { private boolean checkForDifferencesWithProgram() { List analyzerStates = model.getModelData(); boolean changes = false; - for (int i = 0; i < analyzerStates.size(); ++i) { - String analyzerName = analyzerStates.get(i).getName(); - boolean currEnabled = analyzerStates.get(i).isEnabled(); + for (AnalyzerEnablementState analyzerState : analyzerStates) { + String analyzerName = analyzerState.getName(); + boolean currEnabled = analyzerState.isEnabled(); boolean origEnabled = analysisOptions.getBoolean(analyzerName, false); if (currEnabled != origEnabled) { changes = true;