Compare commits

...

15 commits

Author SHA1 Message Date
Ryan Kurtz
ccefcc1f70 Merge remote-tracking branch 'origin/Ghidra_12.0' 2025-09-29 12:01:19 -04:00
Ryan Kurtz
b0f3dea8d6 GP-0: More WhatsNew improvements 2025-09-29 11:53:54 -04:00
Ryan Kurtz
e4e2df4a09 Merge remote-tracking branch 'origin/GP-0-dragonmacher-test-fixes-9-29-25' 2025-09-29 10:43:51 -04:00
Ryan Kurtz
97dcd914e8 Merge remote-tracking branch 'origin/Ghidra_12.0' 2025-09-29 10:42:33 -04:00
Ryan Kurtz
90e9d803f8 GP-0: New code block format in html produced from markdown 2025-09-29 10:40:55 -04:00
dragonmacher
5c5f577560 Test fixes 2025-09-29 10:33:10 -04:00
Ryan Kurtz
16401d6231 GP-0: WhatsNew corrections 2025-09-29 09:49:24 -04:00
ghidra1
0234da5db5 GP-1 Correct NPE with symbol filter restore 2025-09-29 09:43:03 -04:00
Ryan Kurtz
a3b327c411 Merge remote-tracking branch 'origin/GP-1-dragonmacher-tool-restore-fix' 2025-09-29 05:15:02 -04:00
Ryan Kurtz
15760581ad Merge remote-tracking branch 'origin/Ghidra_12.0' 2025-09-29 05:14:16 -04:00
Ryan Kurtz
d7bfd098b9 Merge remote-tracking branch 'origin/GP-0_ryanmkurtz_extensions--SQUASHED' into Ghidra_12.0 2025-09-29 05:12:09 -04:00
Ryan Kurtz
605d579070 GP-0: Fixing enablement of installed extensions in dev mode 2025-09-29 05:11:39 -04:00
dragonmacher
6660cc75f1 Fix for recent tool xml restoring change 2025-09-26 20:03:31 -04:00
Ryan Kurtz
e8ba7c3e45 Merge remote-tracking branch 'origin/GP-5992-dragonmacher-options-age-off--SQUASHED' 2025-09-26 13:01:48 -04:00
dragonmacher
b4ba97c3d2 GP-5992 - Added the ability age-off options instead of relying only on registration 2025-09-26 11:33:51 -04:00
17 changed files with 629 additions and 222 deletions

View file

@ -100,59 +100,20 @@ can also be used in headless mode with the new `-mirror` command line option.
## PyGhidra
PyGhidra 3.0.0 (compatible with Ghidra 12.0 and later) introduces many new Python-specific API
methods with the goal of making the most common Ghidra tasks quick and easy, such as opening a
project, getting a program, and running a GhidraScript. Legacy API fuctions such as
project, getting a program, and running a GhidraScript. Legacy API functions such as
`pyghidra.open_program()` and `pyghidra_run_script()` have been deprecated in favor of the new
methods. Below is an example program that showcases some of the new API functionality. See the
PyGhidra library README for more information.
```python
import os, jpype, pyghidra
pyghidra.start()
methods, which are outlined at https://pypi.org/project/pyghidra.
# Open/create a project
with pyghidra.open_project(os.environ["GHIDRA_PROJECT_DIR"], "ExampleProject", create=True) as project:
# Walk a Ghidra release zip file, load every decompiler binary, and save them to the project
with pyghidra.open_filesystem(f"{os.environ['DOWNLOADS_DIR']}/ghidra_11.4_PUBLIC_20250620.zip") as fs:
loader = pyghidra.program_loader().project(project)
for f in fs.files(lambda f: "os/" in f.path and f.name.startswith("decompile")):
loader = loader.source(f.getFSRL()).projectFolderPath("/" + f.parentFile.name)
with loader.load() as load_results:
load_results.save(pyghidra.monitor())
# Analyze the windows decompiler program for a maximum of 10 seconds
with pyghidra.program_context(project, "/win_x86_64/decompile.exe") as program:
analysis_props = pyghidra.analysis_properties(program)
with pyghidra.transaction(program):
analysis_props.setBoolean("Non-Returning Functions - Discovered", False)
analysis_log = pyghidra.analyze(program, pyghidra.monitor(10))
program.save("Analyzed", pyghidra.monitor())
# Walk the project and set a property in each decompiler program
def set_property(domain_file, program):
with pyghidra.transaction(program):
program_info = pyghidra.program_info(program)
program_info.setString("PyGhidra Property", "Set by PyGhidra!")
program.save("Setting property", pyghidra.monitor())
pyghidra.walk_programs(project, set_property, program_filter=lambda f, p: p.name.startswith("decompile"))
# Load some bytes as a new program
ByteArrayCls = jpype.JArray(jpype.JByte)
my_bytes = ByteArrayCls(b"\xaa\xbb\xcc\xdd\xee\xff")
loader = pyghidra.program_loader().project(project).source(my_bytes).name("my_bytes")
loader = loader.loaders("BinaryLoader").language("DATA:LE:64:default")
with loader.load() as load_results:
load_results.save(pyghidra.monitor())
# Run a GhidraScript
pyghidra.ghidra_script(f"{os.environ['GHIDRA_SCRIPTS_DIR']}/HelloWorldScript.java", project)
```
The default Python scripting engine has been changed in Ghidra 12.0 from Jython to PyGhidra.
Existing Jython scripts will need to include the `# @runtime Jython` script header in order to
continue running within the Jython environment.
## Z3 Concolic Emulation and Symbolic Summary
We've added an experimental Z3-based symbolic emulator, which runs as an "auxilliary" domain to the
We've added an experimental Z3-based symbolic emulator, which runs as an "auxiliary" domain to the
concrete emulator, effectively constructing what is commonly called a "concolic" emulator. The
symbolic emulator creates Z3 expressions and branching constraints, but it only follows the path
determined by concrete emulation. This is most easily accessed by installing the "SymbolicSummaryZ3"
extension (**File** → **Install Extensions**) and then enabling the `Z3SummaryPlugin` in the
extension (**File** -> **Install Extensions**) and then enabling the `Z3SummaryPlugin` in the
Debugger or Emulator tool, which includes a GUI for viewing and sorting through the results. The Z3
emulator requires z3-4.13.0, available from https://github.com/Z3Prover/z3. Other versions may work,
but our current test configuration uses 4.13.0. Depending on the release and your platform, the

View file

@ -270,7 +270,9 @@ public class NewSymbolFilter implements SymbolFilter {
for (Element child : children) {
String childName = child.getAttributeValue(Filter.NAME_ATTRIBUTE);
Filter f = filterMap.get(childName);
f.restoreFromXml(child);
if (f != null) { // NOTE: filter definition may have been dropped and not found
f.restoreFromXml(child);
}
}
rebuildActiveFilters();

View file

@ -4,9 +4,9 @@
* 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.
@ -129,30 +129,6 @@ public class ToolPluginOptionsTest extends AbstractGhidraHeadedIntegrationTest {
}
}
@Test
public void testOptionsWithoutRegisteredOwnerGoAway() {
//
// Test that an option value that is not set or read will disappear after saving the
// owning tool.
//
Options options = loadSearchOptions();
Pair<String, String> changedOption = changeStringTestOption(options);
//
// See if the options are there again after saving and reloading. They should be there,
// since the previous operation set the value. We are careful here to simply check
// for the options existence, but not to retrieve it, as doing so would trigger the
// option to be stored again.
//
options = saveAndLoadOptions();
verifyStringOptionStillChanged_WithoutUsingOptionsAPI(options, changedOption.first);
options = saveAndLoadOptions();
verifyUnusedOptionNoLongerHasEntry(options, changedOption.first);
}
@Test
public void testSaveOnlyNonDefaultOptions() {
ToolOptions options = loadSearchOptions();
@ -177,6 +153,8 @@ public class ToolPluginOptionsTest extends AbstractGhidraHeadedIntegrationTest {
// Repeatedly save/load options, accessing them each time, and make sure that they
// re-appear each load.
//
// Note: options removal is now controlled through use of an age-off mechanism
//
Options options = loadSearchOptions();
@ -194,11 +172,11 @@ public class ToolPluginOptionsTest extends AbstractGhidraHeadedIntegrationTest {
verifyStringOptionsStillChanged_UsingTheOptionsAPI(options, changedOption);
//
// now save twice in a row without accessing and the untouched option should be gone
// now save twice in a row without accessing and the untouched option should *not* be gone
//
options = saveAndLoadOptions();
options = saveAndLoadOptions();
verifyUnusedOptionNoLongerHasEntry(options, changedOption.first);
verifyStringOptionsStillChanged_UsingTheOptionsAPI(options, changedOption);
}
@Test

View file

@ -24,6 +24,8 @@ import java.awt.event.KeyEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyEditorSupport;
import java.io.File;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.*;
import javax.swing.JComponent;
@ -34,7 +36,7 @@ import org.junit.*;
import generic.test.AbstractGuiTest;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.util.HelpLocation;
import ghidra.util.*;
import ghidra.util.bean.opteditor.OptionsVetoException;
import ghidra.util.exception.InvalidInputException;
import gui.event.MouseBinding;
@ -454,12 +456,133 @@ public class OptionsTest extends AbstractGuiTest {
assertTrue(options.contains("Foo"));
assertTrue(options.contains("Bar"));
options.registerOption("Bar", 0, null, "foo");
options.registerOption("Bar", 0, null, "Bar description");
options.removeUnusedOptions();
assertFalse(options.contains("Foo"));
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, but not aged-off; should stay around
assertTrue(options.contains("Foo"));
// Save, edit the date of the options to be older than 1 year, which is our age cutoff.
// Make sure any unregistered option is then removed.
Element savedRoot = options.getXmlRoot(false);
LocalDate twoYearsAgo = LocalDate.now().minusYears(2);
Set<String> optionNames = Set.of("Bar", "Foo");
setLastUsedDate(savedRoot, twoYearsAgo, optionNames);
options = new ToolOptions(savedRoot);
assertTrue(options.contains("Foo"));
assertTrue(options.contains("Bar"));
options.registerOption("Bar", 0, null, "Bar description");
options.removeUnusedOptions();
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, and older than 1 year; should be removed
assertFalse(options.contains("Foo"));
}
@Test
public void testRemoveUnusedOption_WrappedOption() throws Exception {
Date date = new Date();
options.setDate("Foo", date);
options.setDate("Bar", date);
saveAndRestoreOptions();
assertEquals(date, options.getDate("Foo", null));
assertEquals(date, options.getDate("Bar", null));
Date defaultDate = new SimpleDateFormat("yyyy-MM-dd").parse("2025-09-12");
options.registerOption("Bar", defaultDate, null, "Bar description");
options.removeUnusedOptions();
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, but not aged-off; should stay around
assertTrue(options.contains("Foo"));
// Save, edit the date of the options to be older than 1 year, which is our age cutoff.
// Make sure any unregistered option is then removed.
Element savedRoot = options.getXmlRoot(false);
LocalDate twoYearsAgo = LocalDate.now().minusYears(2);
Set<String> optionNames = Set.of("Bar", "Foo");
setLastUsedDate(savedRoot, twoYearsAgo, optionNames);
options = new ToolOptions(savedRoot);
assertTrue(options.contains("Foo"));
assertTrue(options.contains("Bar"));
options.registerOption("Bar", defaultDate, null, "Bar description");
options.removeUnusedOptions();
// registered; should stay around
assertTrue(options.contains("Bar"));
// not registered, and older than 1 year; should be removed
assertFalse(options.contains("Foo"));
}
@Test
public void testUnregisteredWarningMessage() {
/*
Test that options access, but not registered, will trigger a warning message.
*/
SpyErrorLogger spyLogger = new SpyErrorLogger();
Msg.setErrorLogger(spyLogger);
options.getInt("Foo", 1);
assertTrue(spyLogger.isEmtpy());
options.validateOptions();
spyLogger.assertLogMessage("Unregistered", "property", "Foo");
spyLogger.reset();
options.registerOption("Foo", 0, null, "Description");
options.getInt("Foo", 2);
options.validateOptions();
assertTrue(spyLogger.isEmtpy());
}
private void setLastUsedDate(Element savedRoot, LocalDate lastRegisteredDate,
Set<String> names) {
/*
Msg.debug(this, XmlUtilities.toString(savedRoot));
<CATEGORY NAME="Test">
<SAVE_STATE NAME="Bar" TYPE="int" VALUE="3" LAST_REGISTERED="2025-09-17" />
<SAVE_STATE NAME="Bar" TYPE="boolean" VALUE="false" LAST_REGISTERED="2025-09-17" />
</CATEGORY>
*/
String dateString = lastRegisteredDate.format(ToolOptions.LAST_REGISTERED_DATE_FORMATTER);
List<Element> elements = getElementsByName(savedRoot, names);
for (Element element : elements) { // <SAVE_STATE NAME="Foo" ...>
element.setAttribute(ToolOptions.LAST_REGISTERED_DATE_ATTIBUTE, dateString);
}
}
@SuppressWarnings("unchecked")
private List<Element> getElementsByName(Element savedRoot, Set<String> names) {
List<Element> matches = new ArrayList<>();
List<Element> children = savedRoot.getChildren();
for (Element saveState : children) {
String name = saveState.getAttributeValue("NAME");
if (names.contains(name)) {
matches.add(saveState);
}
}
return matches;
}
@Test

View file

@ -538,7 +538,7 @@ public class DecompileOptions {
grabFromProgram(program);
// assuming if one is not registered, then none area
// assuming if one is not registered, then none are
if (!opt.isRegistered(PREDICATE_OPTIONSTRING)) {
return;
}

View file

@ -0,0 +1,154 @@
/* ###
* 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.util.*;
import java.util.Map.Entry;
import org.jdom.Attribute;
import org.jdom.Element;
/**
* A version of {@link SaveState} that allows clients to add attributes to properties in this save
* state. The following code shows how to use this class:
* <pre>
* AttributedSaveState ss = new AttributedSaveState();
* ss.putBoolean("Happy", true);
*
* Map<String, String> attrs = Map.of("MyAttribute", "AttributeValue");
* ss.addAttrbibutes("Happy", attrs);
* </pre>
*
* <p>In this example, the property "Happy" will be given the attribute "MyAttribute" with the value
* of "AttributeValue". This is useful for clients that wish to add attributes to individual
* properties, such as a date for tracking usage.
*
* <p><u>Usage Note:</u> The given attributes are only supported when writing and reading xml. Json
* is not supported.
*/
public class AttributedSaveState extends SaveState {
private Map<String, Map<String, String>> propertyAttributes;
public AttributedSaveState() {
super();
}
public AttributedSaveState(String name) {
super(name);
}
public AttributedSaveState(Element root) {
super(root);
}
/**
* Adds the given map of attribute name/value pairs to this save state.
* @param propertyName the property name within this save state that will be attributed
* @param attributes the attributes
*/
public void addAttributes(String propertyName, Map<String, String> attributes) {
getPropertyAttributes().put(propertyName, attributes);
}
/**
* Removes all attributes associated with the given property name.
* @param propertyName the property name within this save state that has the given attributes
*/
public void removeAttributes(String propertyName) {
getPropertyAttributes().remove(propertyName);
}
/**
* Gets the attributes currently associated with the given property name
* @param propertyName the property name for which to get attributes
* @return the attributes or null
*/
public Map<String, String> getAttributes(String propertyName) {
return getPropertyAttributes().get(propertyName);
}
private Map<String, Map<String, String>> getPropertyAttributes() {
if (propertyAttributes == null) {
propertyAttributes = new HashMap<>();
}
return propertyAttributes;
}
@Override
protected SaveState createSaveState() {
return new AttributedSaveState();
}
@Override
protected void initializeElement(Element e) {
String name = e.getAttributeValue(NAME);
if (name == null) {
return; // sub-element; properties not supported
}
//
// Overridden to add our attributes to the newly created element, used to create xml
//
Map<String, String> attrs = getPropertyAttributes().get(name);
if (attrs != null) {
Set<Entry<String, String>> entries = attrs.entrySet();
for (Entry<String, String> entry : entries) {
String key = entry.getKey();
String value = entry.getValue();
e.setAttribute(key, value);
}
}
}
@Override
protected void processElement(Element element) {
super.processElement(element);
String name = element.getAttributeValue(NAME);
if (name == null) {
return; // sub-element; properties not supported
}
//
// Overridden to extract non-standard attributes from the given element. The element was
// created after restoring from xml. We extract the attributes that we added above in
// initializeElement().
//
Map<String, String> newAttrs = new HashMap<>();
@SuppressWarnings("unchecked")
List<Attribute> attrs = element.getAttributes();
for (Attribute attr : attrs) {
String attrName = attr.getName();
String attrValue = switch (attrName) {
// ignore standard attributes, as they are managed by the parent class
case NAME, TYPE, VALUE -> null;
default -> attr.getValue();
};
if (attrValue != null) {
newAttrs.put(attrName, attrValue);
}
}
if (!newAttrs.isEmpty()) {
getPropertyAttributes().put(name, newAttrs);
}
}
}

View file

@ -78,7 +78,7 @@ public class GProperties {
private static final String STATE = "STATE";
protected static final String TYPE = "TYPE";
protected static final String NAME = "NAME";
private static final String VALUE = "VALUE";
protected static final String VALUE = "VALUE";
public static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
private static final String ARRAY_ELEMENT_NAME = "A";
protected TreeMap<String, Object> map; // use ordered map for deterministic serialization
@ -505,35 +505,34 @@ public class GProperties {
return root;
}
protected Element createElement(String key, Object value) {
protected Element createElement(String propertyName, Object value) {
Element elem = null;
if (value instanceof Element) {
elem = createElementFromElement(key, (Element) value);
elem = createElementFromElement(propertyName, (Element) value);
}
else if (value instanceof Byte) {
elem = setAttributes(key, "byte", ((Byte) value).toString());
elem = setAttributes(propertyName, "byte", ((Byte) value).toString());
}
else if (value instanceof Short) {
elem = setAttributes(key, "short", ((Short) value).toString());
elem = setAttributes(propertyName, "short", ((Short) value).toString());
}
else if (value instanceof Integer) {
elem = setAttributes(key, "int", ((Integer) value).toString());
elem = setAttributes(propertyName, "int", ((Integer) value).toString());
}
else if (value instanceof Long) {
elem = setAttributes(key, "long", ((Long) value).toString());
elem = setAttributes(propertyName, "long", ((Long) value).toString());
}
else if (value instanceof Float) {
elem = setAttributes(key, "float", ((Float) value).toString());
elem = setAttributes(propertyName, "float", ((Float) value).toString());
}
else if (value instanceof Double) {
elem = setAttributes(key, "double", ((Double) value).toString());
elem = setAttributes(propertyName, "double", ((Double) value).toString());
}
else if (value instanceof Boolean) {
elem = setAttributes(key, "boolean", ((Boolean) value).toString());
elem = setAttributes(propertyName, "boolean", ((Boolean) value).toString());
}
else if (value instanceof String) {
elem = new Element(STATE);
elem.setAttribute(NAME, key);
elem = createElement(STATE, propertyName);
elem.setAttribute(TYPE, "string");
if (XmlUtilities.hasInvalidXMLCharacters((String) value)) {
elem.setAttribute("ENCODED_VALUE", NumericUtilities
@ -544,71 +543,66 @@ public class GProperties {
}
}
else if (value instanceof Color) {
elem = setAttributes(key, "Color", Integer.toString(((Color) value).getRGB()));
elem = setAttributes(propertyName, "Color", Integer.toString(((Color) value).getRGB()));
}
else if (value instanceof Date) {
elem = setAttributes(key, "Date", DATE_FORMAT.format((Date) value));
elem = setAttributes(propertyName, "Date", DATE_FORMAT.format((Date) value));
}
else if (value instanceof File) {
elem = setAttributes(key, "File", ((File) value).getAbsolutePath());
elem = setAttributes(propertyName, "File", ((File) value).getAbsolutePath());
}
else if (value instanceof KeyStroke) {
elem = setAttributes(key, "KeyStroke", value.toString());
elem = setAttributes(propertyName, "KeyStroke", value.toString());
}
else if (value instanceof Font font) {
elem = setAttributes(key, "Font", toFontString(font));
elem = setAttributes(propertyName, "Font", toFontString(font));
}
else if (value instanceof byte[]) {
elem = new Element("BYTES");
elem.setAttribute(NAME, key);
elem = createElement("BYTES", propertyName);
elem.setAttribute(VALUE, NumericUtilities.convertBytesToString((byte[]) value));
}
else if (value instanceof short[]) {
elem = setArrayAttributes(key, "short", value);
elem = setArrayAttributes(propertyName, "short", value);
}
else if (value instanceof int[]) {
elem = setArrayAttributes(key, "int", value);
elem = setArrayAttributes(propertyName, "int", value);
}
else if (value instanceof long[]) {
elem = setArrayAttributes(key, "long", value);
elem = setArrayAttributes(propertyName, "long", value);
}
else if (value instanceof float[]) {
elem = setArrayAttributes(key, "float", value);
elem = setArrayAttributes(propertyName, "float", value);
}
else if (value instanceof double[]) {
elem = setArrayAttributes(key, "double", value);
elem = setArrayAttributes(propertyName, "double", value);
}
else if (value instanceof boolean[]) {
elem = setArrayAttributes(key, "boolean", value);
elem = setArrayAttributes(propertyName, "boolean", value);
}
else if (value instanceof String[]) {
elem = setArrayAttributes(key, "string", value);
elem = setArrayAttributes(propertyName, "string", value);
}
else if (value instanceof Enum) {
Enum<?> e = (Enum<?>) value;
elem = new Element("ENUM");
elem.setAttribute(NAME, key);
elem = createElement("ENUM", propertyName);
elem.setAttribute(TYPE, "enum");
elem.setAttribute("CLASS", e.getClass().getName());
elem.setAttribute(VALUE, e.name());
}
else if (value instanceof GProperties) {
Element savedElement = ((GProperties) value).saveToXml();
elem = new Element(GPROPERTIES_TAG);
elem.setAttribute(NAME, key);
elem = createElement(GPROPERTIES_TAG, propertyName);
elem.setAttribute(TYPE, G_PROPERTIES_TYPE);
elem.addContent(savedElement);
}
else {
elem = new Element("NULL");
elem.setAttribute(NAME, key);
elem = createElement("NULL", propertyName);
}
return elem;
}
private <T> Element setArrayAttributes(String key, String type, Object values) {
Element elem = new Element("ARRAY");
elem.setAttribute(NAME, key);
private <T> Element setArrayAttributes(String propertyName, String type, Object values) {
Element elem = createElement("ARRAY", propertyName);
elem.setAttribute(TYPE, type);
for (int i = 0; i < Array.getLength(values); i++) {
Object value = Array.get(values, i);
@ -621,10 +615,19 @@ public class GProperties {
return elem;
}
private Element setAttributes(String key, String type, String value) {
Element elem;
elem = new Element(STATE);
elem.setAttribute(NAME, key);
protected Element createElement(String tag, String name) {
Element e = new Element(tag);
e.setAttribute(NAME, name);
initializeElement(e);
return e;
}
protected void initializeElement(Element e) {
// subclasses may override
}
private Element setAttributes(String propertyName, String type, String value) {
Element elem = createElement(STATE, propertyName);
elem.setAttribute(TYPE, type);
elem.setAttribute(VALUE, value);
return elem;
@ -792,8 +795,7 @@ public class GProperties {
}
protected Element createElementFromElement(String internalKey, Element internalElement) {
Element newElement = new Element("XML");
newElement.setAttribute(NAME, internalKey);
Element newElement = createElement("XML", internalKey);
Element internalElementClone = (Element) internalElement.clone();
newElement.addContent(internalElementClone);
@ -1420,11 +1422,11 @@ public class GProperties {
return true;
}
private String toFontString(Font font) {
private static String toFontString(Font font) {
return String.format("%s-%s-%s", font.getName(), getStyleString(font), font.getSize());
}
private String getStyleString(Font font) {
private static String getStyleString(Font font) {
boolean bold = font.isBold();
boolean italic = font.isItalic();
if (bold && italic) {

View file

@ -17,6 +17,7 @@ package ghidra.framework.options;
import java.io.File;
import java.io.IOException;
import java.util.List;
import org.jdom.Element;
@ -114,31 +115,86 @@ public class SaveState extends XmlProperties {
return getAsType(name, null, SaveState.class);
}
@SuppressWarnings("unchecked")
@Override
protected void processElement(Element element) {
String tag = element.getName();
if (tag.equals("SAVE_STATE")) {
Element child = (Element) element.getChildren().get(0);
if (child != null) {
String name = element.getAttributeValue(NAME);
map.put(name, new SaveState(child));
return;
}
if (!tag.equals("SAVE_STATE")) {
super.processElement(element);
return;
}
super.processElement(element);
/*
When using a SaveState inside of a SaveState, we produce xml that looks like this:
<SAVE_STATE NAME="Bar" TYPE="SaveState">
<STATE NAME="Bar" TYPE="int" VALUE="3" />
</SAVE_STATE>
*/
SaveState saveState = createSaveState();
List<Element> children = element.getChildren();
if (children.isEmpty()) {
return;
}
Element child = (Element) element.getChildren().get(0);
String childTag = child.getName();
if (childTag.equals("SAVE_STATE")) {
/*
Old style tag, with one level of extra nesting
<SAVE_STATE NAME="Bar" TYPE="SaveState">
<SAVE_STATE>
<STATE NAME="DATED_OPTION" TYPE="int" VALUE="3" />
</SAVE_STATE>
</SAVE_STATE>
*/
children = child.getChildren();
}
for (Element e : children) {
saveState.processElement(e);
}
String parentName = element.getAttributeValue(NAME);
map.put(parentName, saveState);
}
@SuppressWarnings("unchecked")
@Override
protected Element createElement(String key, Object value) {
if (value instanceof SaveState saveState) {
Element savedElement = saveState.saveToXml();
Element element = new Element("SAVE_STATE");
element.setAttribute(NAME, key);
element.setAttribute(TYPE, "SaveState");
element.addContent(savedElement);
return element;
if (!(value instanceof SaveState saveState)) {
return super.createElement(key, value);
}
return super.createElement(key, value);
/*
When using a SaveState inside of a SaveState, we produce xml that looks like this:
<SAVE_STATE NAME="Bar" TYPE="SaveState">
<STATE NAME="Bar" TYPE="int" VALUE="3" />
</SAVE_STATE>
*/
Element savedElement = saveState.saveToXml();
Element element = new Element("SAVE_STATE");
element.setAttribute(NAME, key);
element.setAttribute(TYPE, "SaveState");
// do not write an extra <SAVE_STATE> intermediate node
List<Element> children = savedElement.getChildren();
for (Element e : children) {
Element newElement = (Element) e.clone();
element.addContent(newElement);
}
return element;
}
// allows subclasses to override how sub-save states are created
protected SaveState createSaveState() {
return new SaveState();
}
}

View file

@ -4,9 +4,9 @@
* 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.
@ -268,10 +268,8 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
}
/**
* Returns true if this extension is installed under an installation folder or inside of a
* source control repository folder.
* @return true if this extension is installed under an installation folder or inside of a
* source control repository folder.
* {@return true if this extension is installed under an installation folder or inside of a
* source control repository folder}
*/
public boolean isInstalledInInstallationFolder() {
if (installDir == null) {
@ -287,11 +285,10 @@ public class ExtensionDetails implements Comparable<ExtensionDetails> {
}
// extDirs.get(0) is the user extension dir
ResourceFile appExtDir = extDirs.get(1);
if (FileUtilities.isPathContainedWithin(appExtDir.getFile(false), installDir)) {
return true;
}
return false;
return extDirs.stream()
.skip(1)
.anyMatch(
dir -> FileUtilities.isPathContainedWithin(dir.getFile(false), installDir));
}
/**

View file

@ -4,9 +4,9 @@
* 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.
@ -15,7 +15,7 @@
*/
package ghidra.util;
import static org.junit.Assert.fail;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.Iterator;
@ -88,6 +88,10 @@ public class SpyErrorLogger implements ErrorLogger, Iterable<String> {
messages.clear();
}
public boolean isEmtpy() {
return messages.isEmpty();
}
public void assertLogMessage(String... words) {
for (String message : this) {
if (StringUtilities.containsAllIgnoreCase(message, words)) {

View file

@ -260,8 +260,11 @@ public abstract class AbstractOptions implements Options {
if (option == null) {
return null;
}
if (option.getOptionType() != type) {
Msg.error(this, "Registered option incompatible with existing option: " + optionName,
OptionType existingType = option.getOptionType();
if (existingType != type) {
Msg.error(this, "Registered option incompatible with existing option: '%s'. " +
"Existing type '%s'; registered type '%s'.".formatted(optionName, existingType,
type),
new AssertException());
return null;
}

View file

@ -16,6 +16,7 @@
package ghidra.framework.options;
import java.beans.PropertyEditor;
import java.time.LocalDate;
import java.util.Objects;
import ghidra.util.HelpLocation;
@ -23,11 +24,15 @@ import ghidra.util.SystemUtilities;
import utilities.util.reflection.ReflectionUtilities;
public abstract class Option {
public static final String UNREGISTERED_OPTION = "Unregistered Option";
private static final LocalDate ONE_YEAR_AGO = LocalDate.now().minusYears(1);
private final String name;
private Object defaultValue;
private boolean isRegistered;
private LocalDate lastRegisteredDate;
private String description;
private HelpLocation helpLocation;
private OptionType optionType;
@ -45,7 +50,10 @@ public abstract class Option {
this.defaultValue = defaultValue;
this.isRegistered = isRegistered;
this.propertyEditor = editor;
if (!isRegistered) {
if (isRegistered) {
lastRegisteredDate = LocalDate.now();
}
else {
recordInception();
}
}
@ -67,6 +75,7 @@ public abstract class Option {
defaultValue = defaultValue != null ? defaultValue : updatedDefaultValue;
propertyEditor = propertyEditor != null ? propertyEditor : updatedEditor;
isRegistered = true;
lastRegisteredDate = LocalDate.now();
}
public abstract Object getCurrentValue();
@ -74,7 +83,8 @@ public abstract class Option {
public abstract void doSetCurrentValue(Object value);
public void setCurrentValue(Object value) {
this.isRegistered = true;
isRegistered = true;
lastRegisteredDate = LocalDate.now();
doSetCurrentValue(value);
}
@ -109,10 +119,44 @@ public abstract class Option {
return value;
}
public boolean wasRegisteredInPreviousSession() {
return lastRegisteredDate != null;
}
public boolean isRegistered() {
return isRegistered;
}
public void setLastRegisteredDate(LocalDate date) {
lastRegisteredDate = date;
}
public LocalDate getLastRegisteredDate() {
LocalDate date = lastRegisteredDate;
if (date == null) {
// This implies a new option that has not been registered and was not in a saved tool.
// Pick an old date so the option will not linger.
date = ONE_YEAR_AGO;
}
return date;
}
/**
* Returns true if the last registered date for this option is older than 1 year ago. That
* means that the option has not been registered and is likely no longer valid. This may not be
* true, if the given option still exists, but is only active on-demand by the user. If the
* option has expired, it will be removed. If it is still an existing on-demand option, it can
* again be saved when the user loads the owning provider and changes the option. In this case,
* it will remain in the tool for at least another year.
* @return true if expired
*/
public boolean hasExpired() {
if (lastRegisteredDate == null) {
return false;
}
return lastRegisteredDate.isBefore(ONE_YEAR_AGO);
}
public void restoreDefault() {
setCurrentValue(defaultValue);
}
@ -151,4 +195,5 @@ public abstract class Option {
public OptionType getOptionType() {
return optionType;
}
}

View file

@ -20,6 +20,8 @@ import java.awt.Font;
import java.beans.PropertyEditor;
import java.io.File;
import java.lang.reflect.Constructor;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import javax.swing.KeyStroke;
@ -45,10 +47,16 @@ import ghidra.util.exception.AssertException;
* <p>The Options Dialog shows the delimited hierarchy in tree format.
*/
public class ToolOptions extends AbstractOptions {
private static final String CLASS_ATTRIBUTE = "CLASS";
private static final String NAME_ATTRIBUTE = "NAME";
private static final String WRAPPED_OPTION_NAME = "WRAPPED_OPTION";
private static final String CLEARED_VALUE_ELEMENT_NAME = "CLEARED_VALUE";
public static final String LAST_REGISTERED_DATE_ATTIBUTE = "LAST_REGISTERED";
public static final DateTimeFormatter LAST_REGISTERED_DATE_FORMATTER =
DateTimeFormatter.ISO_LOCAL_DATE;
public static final Set<Class<?>> PRIMITIVE_CLASSES = buildPrimitiveClassSet();
public static final Set<Class<?>> WRAPPABLE_CLASSES = buildWrappableClassSet();
@ -91,7 +99,7 @@ public class ToolOptions extends AbstractOptions {
public ToolOptions(Element root) {
this(root.getAttributeValue(NAME_ATTRIBUTE));
SaveState saveState = new SaveState(root);
AttributedSaveState saveState = new AttributedSaveState(root);
readNonWrappedOptions(saveState);
@ -103,16 +111,35 @@ public class ToolOptions extends AbstractOptions {
}
}
private void readNonWrappedOptions(SaveState saveState) {
private void readNonWrappedOptions(AttributedSaveState saveState) {
for (String optionName : saveState.getNames()) {
Object object = saveState.getObject(optionName);
Option option =
createUnregisteredOption(optionName, OptionType.getOptionType(object), null);
OptionType type = OptionType.getOptionType(object);
Option option = createUnregisteredOption(optionName, type, null);
option.doSetCurrentValue(object); // use doSet versus set so that it is not registered
LocalDate date = getLastRegisteredDateString(saveState, optionName);
option.setLastRegisteredDate(date);
valueMap.put(optionName, option);
}
}
private LocalDate getLastRegisteredDateString(AttributedSaveState saveState,
String optionName) {
// Get the last registered date. No date implies an old xml format.
Map<String, String> attrs = saveState.getAttributes(optionName);
if (attrs != null) {
String dateString = attrs.get(LAST_REGISTERED_DATE_ATTIBUTE);
if (dateString != null) {
return LocalDate.parse(dateString, LAST_REGISTERED_DATE_FORMATTER);
}
}
return LocalDate.now();
}
private void readWrappedOptions(Element root) throws ReflectiveOperationException {
Iterator<?> it = root.getChildren(WRAPPED_OPTION_NAME).iterator();
@ -124,19 +151,19 @@ public class ToolOptions extends AbstractOptions {
continue; // shouldn't happen
}
String optionName = element.getAttributeValue(NAME_ATTRIBUTE);
Class<?> c = Class.forName(element.getAttributeValue(CLASS_ATTRIBUTE));
Constructor<?> constructor = c.getDeclaredConstructor();
WrappedOption wo = (WrappedOption) constructor.newInstance();
wo.readState(new SaveState(element));
if (wo instanceof WrappedCustomOption wrappedCustom && !wrappedCustom.isValid()) {
continue;
}
if (wo instanceof WrappedKeyStroke wrappedKs) {
wo = wrappedKs.toWrappedActionTrigger();
}
String optionName = element.getAttributeValue(NAME_ATTRIBUTE);
Option option = createUnregisteredOption(optionName, wo.getOptionType(), null);
valueMap.put(optionName, option);
@ -149,6 +176,15 @@ public class ToolOptions extends AbstractOptions {
else {
option.doSetCurrentValue(wo.getObject()); // use doSet so that it is not registered
}
// Get the last registered date. No date implies an old xml format.
LocalDate date = LocalDate.now();
String dateString = element.getAttributeValue(LAST_REGISTERED_DATE_ATTIBUTE);
if (dateString != null) {
date = LocalDate.parse(dateString, LAST_REGISTERED_DATE_FORMATTER);
}
option.setLastRegisteredDate(date);
}
}
@ -162,7 +198,7 @@ public class ToolOptions extends AbstractOptions {
*/
public Element getXmlRoot(boolean includeDefaultBindings) {
SaveState saveState = new SaveState(XML_ELEMENT_NAME);
AttributedSaveState saveState = new AttributedSaveState(XML_ELEMENT_NAME);
writeNonWrappedOptions(includeDefaultBindings, saveState);
@ -174,18 +210,31 @@ public class ToolOptions extends AbstractOptions {
return root;
}
private void writeNonWrappedOptions(boolean includeDefaultBindings, SaveState saveState) {
private void writeNonWrappedOptions(boolean includeDefaultBindings,
AttributedSaveState saveState) {
for (String optionName : valueMap.keySet()) {
Option optionValue = valueMap.get(optionName);
if (includeDefaultBindings || !optionValue.isDefault()) {
Object value = optionValue.getValue(null);
if (isSupportedBySaveState(value)) {
saveState.putObject(optionName, value);
}
Option option = valueMap.get(optionName);
if (includeDefaultBindings || !option.isDefault()) {
writeNonWrappedOption(saveState, optionName, option);
}
}
}
private void writeNonWrappedOption(AttributedSaveState saveState, String optionName,
Option option) {
Object value = option.getValue(null);
if (!isSupportedBySaveState(value)) {
return;
}
saveState.putObject(optionName, value);
LocalDate date = option.getLastRegisteredDate();
String dateString = date.format(LAST_REGISTERED_DATE_FORMATTER);
Map<String, String> attrs = Map.of(LAST_REGISTERED_DATE_ATTIBUTE, dateString);
saveState.addAttributes(optionName, attrs);
}
private void writeWrappedOptions(boolean includeDefaultBindings, Element root) {
for (String optionName : valueMap.keySet()) {
Option option = valueMap.get(optionName);
@ -195,36 +244,47 @@ public class ToolOptions extends AbstractOptions {
}
if (includeDefaultBindings || !option.isDefault()) {
Object value = option.getCurrentValue();
if (isSupportedBySaveState(value)) {
continue; // handled above
}
WrappedOption wrappedOption = wrapOption(option);
if (wrappedOption == null) {
continue; // cannot write an option without a value to determine its type
}
SaveState ss = new SaveState(WRAPPED_OPTION_NAME);
Element elem = null;
if (value == null) {
// Handle the null case ourselves, not using the wrapped option (and when
// reading from xml) so the logic does not need to be in each wrapped option
elem = ss.saveToXml();
elem.addContent(new Element(CLEARED_VALUE_ELEMENT_NAME));
}
else {
wrappedOption.writeState(ss);
elem = ss.saveToXml();
}
elem.setAttribute(NAME_ATTRIBUTE, optionName);
elem.setAttribute(CLASS_ATTRIBUTE, wrappedOption.getClass().getName());
root.addContent(elem);
writeWrappedOption(root, optionName, option);
}
}
}
private void writeWrappedOption(Element root, String optionName, Option option) {
Object value = option.getCurrentValue();
if (isSupportedBySaveState(value)) {
return; // handled above
}
WrappedOption wrappedOption = wrapOption(option);
if (wrappedOption == null) {
return; // cannot write an option without a value to determine its type
}
SaveState ss = new SaveState(WRAPPED_OPTION_NAME);
Element element = null;
if (value == null) {
// Handle the null case ourselves, not using the wrapped option (and when
// reading from xml) so the logic does not need to be in each wrapped option
element = ss.saveToXml();
element.addContent(new Element(CLEARED_VALUE_ELEMENT_NAME));
}
else {
wrappedOption.writeState(ss);
element = ss.saveToXml();
}
element.setAttribute(NAME_ATTRIBUTE, optionName);
String className = wrappedOption.getClass().getName();
element.setAttribute(CLASS_ATTRIBUTE, className);
LocalDate date = option.getLastRegisteredDate();
String dateString = date.format(LAST_REGISTERED_DATE_FORMATTER);
element.setAttribute(LAST_REGISTERED_DATE_ATTIBUTE, dateString);
root.addContent(element);
}
private boolean isSupportedBySaveState(Object obj) {
if (obj == null) {
return false;
@ -310,7 +370,7 @@ public class ToolOptions extends AbstractOptions {
List<String> optionNames = new ArrayList<>(valueMap.keySet());
for (String optionName : optionNames) {
Option optionState = valueMap.get(optionName);
if (!optionState.isRegistered()) {
if (optionState.hasExpired()) {
removeOption(optionName);
}
}
@ -377,11 +437,15 @@ public class ToolOptions extends AbstractOptions {
Set<String> keySet = valueMap.keySet();
for (String propertyName : keySet) {
Option optionState = valueMap.get(propertyName);
if (optionState.isRegistered()) {
if (optionState.isRegistered() || optionState.wasRegisteredInPreviousSession()) {
continue;
}
Msg.warn(this, "Unregistered property \"" + propertyName + "\" in Options \"" + name +
"\"\n " + optionState.getInceptionInformation());
// getting here means that this option was used in the current tool session, but was not
// registered this session or in a previous session
Msg.warn(this,
"Unregistered property \"" + propertyName + "\" in Options \"" + name +
"\"\n " + optionState.getInceptionInformation());
}
}

View file

@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -17,30 +16,29 @@
package ghidra.framework.options;
/**
* Wrapper class for an object that represents a property value and is
* saved as a set of primitives.
* Wrapper class for an object that represents a property value and is saved as a set of primitives.
*/
public interface WrappedOption {
/**
* Get the object that is the property value.
* {@return Get the object that is the property value}
*/
public abstract Object getObject();
/**
* Concrete subclass of WrappedOption should read all of its
* state from the given saveState object.
* Subclasses of WrappedOption should read all state from the given save state object.
* @param saveState container of state information
*/
public abstract void readState(SaveState saveState);
/**
* Concrete subclass of WrappedOption should write all of its
* state to the given saveState object.
* Subclasses of WrappedOption should write all state to the given save state object.
* @param saveState container of state information
*/
public abstract void writeState(SaveState saveState);
/**
* {@return the option type}
*/
public abstract OptionType getOptionType();
}

View file

@ -227,9 +227,8 @@ public class GhidraApplicationLayout extends ApplicationLayout {
* Returns a prioritized list of directories where Ghidra extensions are installed. These
* should be at the following locations:<br>
* <ul>
* <li><code>[user settings dir]/Extensions</code></li>
* <li><code>[application install dir]/Ghidra/Extensions</code> (Release Mode)</li>
* <li><code>ghidra/Ghidra/Extensions</code> (Development Mode)</li>
* <li>{@code [user settings dir]/Extensions}</li>
* <li>{@code [application root dirs]/Extensions}</li>
* </ul>
*
* @return the install folder, or null if can't be determined

View file

@ -129,9 +129,8 @@ public abstract class ApplicationLayout {
* Returns a prioritized {@link List ordered list} of the application Extensions installation
* directories. Typically, the values may be any of the following locations:<br>
* <ul>
* <li><code>[user settings dir]/Extensions</code></li>
* <li><code>[application install dir]/Ghidra/Extensions</code> (Release Mode)</li>
* <li><code>ghidra/Ghidra/Extensions</code> (Development Mode)</li>
* <li>{@code [user settings dir]/Extensions}</li>
* <li>{@code [application root dirs]/Extensions}</li>
* </ul>
*
* @return an {@link List ordered list} of the application Extensions installation directories.

View file

@ -127,9 +127,31 @@ public class MarkdownToHtml {
@Override
public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
if (node instanceof Code || node instanceof IndentedCodeBlock ||
node instanceof FencedCodeBlock) {
attributes.put("style", "background-color: #eef;");
// NOTE: This method will get called on both the <pre> and <code> tags, so be careful
// not to apply things twice
if (node instanceof FencedCodeBlock && tagName.equals("pre")) {
StringBuilder sb = new StringBuilder();
sb.append("background: #f4f4f4;");
sb.append("border: 1px solid #ddd;");
sb.append("border-left: 3px solid #f36d33;");
sb.append("color: #666;");
sb.append("display: block;");
sb.append("font-family: monospace;");
sb.append("line-height: 1.6;");
sb.append("margin-bottom: 1.6em;");
sb.append("max-width: 100%;");
sb.append("overflow: auto;");
sb.append("padding: 1em 1.5em;");
sb.append("page-break-inside: avoid;");
sb.append("word-wrap: break-word;");
attributes.put("style", sb.toString());
}
else if (node instanceof Code || node instanceof IndentedCodeBlock) {
StringBuilder sb = new StringBuilder();
sb.append("background: #f4f4f4;");
sb.append("font-family: monospace;");
attributes.put("style", sb.toString());
}
}
}