diff --git a/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html b/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html index 86974ba456..4b7946bd01 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html +++ b/Ghidra/Features/Base/src/main/help/help/topics/Annotations/Annotations.html @@ -104,11 +104,11 @@ Notes:
+ * NOTE: ALL AnnotatedStringHandler class names MUST END IN "StringHandler". If not, the
+ * ClassSearcher will not find them.
*/
public interface AnnotatedStringHandler extends ExtensionPoint {
@@ -44,7 +43,7 @@ public interface AnnotatedStringHandler extends ExtensionPoint {
* @param s string to escape
* @return escaped string
*/
- static String escapeAnnotationPart(String s) {
+ public static String escapeAnnotationPart(String s) {
s = Objects.requireNonNullElse(s, "");
String origStr = s;
if (s.indexOf('"') >= 0) {
@@ -56,14 +55,6 @@ public interface AnnotatedStringHandler extends ExtensionPoint {
return s;
}
- AnnotatedMouseHandler DUMMY_MOUSE_HANDLER = new AnnotatedMouseHandler() {
- @Override
- public boolean handleMouseClick(ProgramLocation location, MouseEvent mouseEvent,
- ServiceProvider serviceProvider) {
- return false;
- }
- };
-
/**
* Creates an {@link FieldElement} based upon the give array of Strings. The first String
* in the list is expected to be the annotation tag used to create the annotation. At the
@@ -80,7 +71,7 @@ public interface AnnotatedStringHandler extends ExtensionPoint {
* @throws AnnotationException if the given text data does not fit the expected format for
* the given handler implementation.
*/
- AttributedString createAnnotatedString(AttributedString prototypeString, String[] text,
+ public AttributedString createAnnotatedString(AttributedString prototypeString, String[] text,
Program program) throws AnnotationException;
/**
@@ -89,42 +80,47 @@ public interface AnnotatedStringHandler extends ExtensionPoint {
*
* @return the annotation string names that this AnnotatedStringHandler supports.
*/
- String[] getSupportedAnnotations();
+ public String[] getSupportedAnnotations();
/**
* A method that is notified when an annotation is clicked. Returns true if this annotation
* handles the click; return false if this annotation does not do anything with the click.
*
- * @param annotationParts The constituent parts of the annotation
- * @param sourceNavigatable The location in the program that was clicked.
- * @param serviceProvider A service provider for needed services.
+ * @param text the constituent parts of the annotation
+ * @param sourceNavigatable the location in the program that was clicked.
+ * @param serviceProvider the service provider for needed services.
* @return true if this annotation handles the click; return false if this annotation does
* not do anything with the click.
*/
- boolean handleMouseClick(String[] annotationParts, Navigatable sourceNavigatable,
+ public boolean handleMouseClick(String[] text, Navigatable sourceNavigatable,
ServiceProvider serviceProvider);
/**
* Returns the String that represents the GUI presence of this option
* @return the String to display in GUI components.
*/
- String getDisplayString();
+ public String getDisplayString();
/**
* Returns an example string of how the annotation is used
* @return the example of how this is used.
*/
- String getPrototypeString();
+ public String getPrototypeString();
/**
* Returns an example string of how the annotation is used
- * @param displayText The text that may be wrapped, cannot be null
+ * @param displayText the text that may be wrapped, will not be null
* @return the example of how this is used.
*/
- default String getPrototypeString(String displayText) {
+ public default String getPrototypeString(String displayText) {
return getPrototypeString();
}
+ @Deprecated(forRemoval = true)
+ public default String[] modify(String[] text, Program program) {
+ return modify(text, program, Address.NO_ADDRESS);
+ }
+
/**
* Returns an array with modifications by the annotation; null otherwise. This method will be
* called by the framework when comments are created, before they are applied. This allows the
@@ -132,9 +128,10 @@ public interface AnnotatedStringHandler extends ExtensionPoint {
*
* @param text the array of annotation parts to modify
* @param program the program
+ * @param addr address of the annotation in the program
* @return the modified array; null otherwise
*/
- default String[] modify(String[] text, Program program) {
+ public default String[] modify(String[] text, Program program, Address addr) {
return null;
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CommentUtils.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CommentUtils.java
index b56eb23c92..d160fb144e 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CommentUtils.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/CommentUtils.java
@@ -50,9 +50,10 @@ public class CommentUtils {
*
* @param rawCommentText the text to be updated
* @param program the program associated with the comment
+ * @param addr address of comment in program
* @return the updated string
*/
- public static String fixupAnnotations(String rawCommentText, Program program) {
+ public static String fixupAnnotations(String rawCommentText, Program program, Address addr) {
if (rawCommentText == null) {
return null;
@@ -66,7 +67,7 @@ public class CommentUtils {
String[] annotationParts = annotation.getAnnotationParts();
AnnotatedStringHandler handler = getAnnotationHandler(annotationParts);
- String[] updatedParts = handler.modify(annotationParts, program);
+ String[] updatedParts = handler.modify(annotationParts, program, addr);
if (updatedParts == null) {
return annotation; // nothing to change
}
@@ -163,8 +164,7 @@ public class CommentUtils {
text = StringUtilities.convertTabsToSpaces(text);
int column = 0;
- List
+ * This allows users to reference function variables in comments without having to change the
+ * comment text when the variable is renamed. Users can enter the annotation using the variable's
+ * name:
+ *
+ * {@variable local_8}
+ *
+ * or
+ *
+ * {@variable coolVariable SomeFunction}
+ *
+ * The user annotation will be converted to use address information:
+ *
+ * {@variable Stack[0xa] FUN_1234eaea}
+ *
+ */
+public class VariableAnnotatedStringHandler implements AnnotatedStringHandler {
+
+ private static final String[] SUPPORTED_ANNOTATIONS = { "variable", "var" };
+
+ @Override
+ public String[] getSupportedAnnotations() {
+ return SUPPORTED_ANNOTATIONS;
+ }
+
+ @Override
+ public String getDisplayString() {
+ return "Variable";
+ }
+
+ @Override
+ public String getPrototypeString() {
+ return "{@variable variable_name}";
+ }
+
+ @Override
+ public String getPrototypeString(String displayText) {
+ return "{@variable " + displayText.trim() + "}";
+ }
+
+ @Override
+ public AttributedString createAnnotatedString(AttributedString prototypeString, String[] text,
+ Program program) throws AnnotationException {
+
+ /*
+ We expect to be handed annotation text that was updated via a previous call to modify().
+ The annotation will be of the form {@variable variable_address function_address}
+ */
+ if (text.length != 3) {
+ throw new AnnotationException(
+ "@variable annotation must have a variable address and a function name");
+ }
+
+ if (program == null) { // this can happen during merge operations
+ return createPlaceholderString(prototypeString, text);
+ }
+
+ String entry = text[2];
+ Function function = getFunctionAt(program, entry);
+ if (function == null) {
+ throw new AnnotationException("Could not find function \"" + entry + "\"");
+ }
+
+ String address = text[1];
+ Variable var = getVariable(function, address);
+ if (var == null) {
+ String functionName = function.getName();
+ throw new AnnotationException(
+ "Could not find variable at address %s in function %s".formatted(functionName,
+ address));
+ }
+
+ return new AttributedString(var.getName(), prototypeString.getColor(0),
+ prototypeString.getFontMetrics(0), true, prototypeString.getColor(0));
+ }
+
+ private AttributedString createPlaceholderString(AttributedString prototypeString,
+ String[] text) {
+ String joined = Arrays.stream(text).collect(Collectors.joining(" "));
+ return new AttributedString(joined, Palette.LIGHT_GRAY,
+ prototypeString.getFontMetrics(0));
+ }
+
+ private Function getFunction(Program program, String value) {
+ FunctionManager manager = program.getFunctionManager();
+ for (Symbol s : NamespaceUtils.getSymbols(value, program)) {
+ Address addr = s.getAddress();
+ Function function = manager.getFunctionAt(addr);
+ if (function != null) {
+ return function;
+ }
+ }
+
+ // if we get here, then see if the value is an address
+ return getFunctionAt(program, value);
+ }
+
+ private Function getFunctionAt(Program program, String address) {
+ FunctionManager manager = program.getFunctionManager();
+ AddressFactory af = program.getAddressFactory();
+ Address addr = af.getAddress(address);
+ if (addr != null) {
+ Function function = manager.getFunctionAt(addr);
+ if (function != null) {
+ return function;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Update the annotation to convert names to addresses.
+ * @param text the array of annotation parts to modify
+ * @param program the program
+ * @param addr address of the annotation in the program
+ * @return the modified array; null otherwise
+ */
+ @Override
+ public String[] modify(String[] text, Program program, Address addr) {
+ if (program == null) { // this can happen during merge operations
+ return null;
+ }
+
+ Function function = null;
+ switch (text.length) {
+ case 3:
+ function = getFunction(program, text[2]);
+ break;
+ case 2:
+ function = program.getFunctionManager().getFunctionContaining(addr);
+ break;
+ default:
+ return null;
+ }
+
+ if (function == null) {
+ return null;
+ }
+
+ String value = text[1]; // value is a variable name or address
+ VariableMatcher matcher = createVariableMatcher(program, value);
+ Variable var = findVariable(function, matcher);
+ if (var == null) {
+ return null;
+ }
+
+ return new String[] {
+ "variable",
+ var.getMinAddress().toString(),
+ function.getEntryPoint().toString()
+ };
+ }
+
+ private VariableMatcher createVariableMatcher(Program p, String value) {
+
+ Address addr = toAddress(p, value);
+ if (addr != null) {
+ return new AddressMatcher(addr);
+ }
+
+ return new NameMatcher(value);
+ }
+
+ @Override
+ public boolean handleMouseClick(String[] text, Navigatable sourceNavigatable,
+ ServiceProvider serviceProvider) {
+
+ /*
+ We expect to be handed annotation text that was updated via a previous call to modify().
+ The annotation will be of the form {@variable variable_address function_address}
+ */
+ if (text.length != 3) {
+ return false;
+ }
+
+ Program program = sourceNavigatable.getProgram();
+ Function function = getFunction(program, text[2]);
+ if (function == null) {
+ return false;
+ }
+
+ String address = text[1];
+ Variable var = getVariable(function, address);
+ if (var == null) {
+ return false;
+ }
+
+ Symbol symbol = var.getSymbol();
+ if (symbol == null) {
+ symbol = function.getSymbol();
+ }
+
+ GoToService goToService = serviceProvider.getService(GoToService.class);
+ if (goToService == null) {
+ Msg.debug(this, "GoToService not installed");
+ return false;
+ }
+
+ return goToService.goTo(symbol.getProgramLocation());
+ }
+
+ private Variable getVariable(Function function, String addressString) {
+ Program p = function.getProgram();
+ Address addr = toAddress(p, addressString);
+ VariableMatcher matcher = new AddressMatcher(addr);
+ return findVariable(function, matcher);
+ }
+
+ private static Variable findVariable(Function function, VariableMatcher matcher) {
+ for (Variable var : function.getAllVariables()) {
+ if (matcher.matches(var)) {
+ return var;
+ }
+ }
+
+ return null;
+ }
+
+ private static Address toAddress(Program p, String s) {
+ AddressFactory af = p.getAddressFactory();
+ return af.getAddress(s);
+ }
+
+ private static interface VariableMatcher {
+ public boolean matches(Variable v);
+ }
+
+ private static class NameMatcher implements VariableMatcher {
+
+ private String name;
+
+ NameMatcher(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean matches(Variable v) {
+ return v.getName().equals(name);
+ }
+ }
+
+ private static class AddressMatcher implements VariableMatcher {
+
+ private Address addr;
+
+ AddressMatcher(Address addr) {
+ this.addr = addr;
+ }
+
+ @Override
+ public boolean matches(Variable v) {
+ return v.getMinAddress().equals(addr);
+ }
+ }
+}
diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java
index 66c2cc8dee..0d3c6d166b 100644
--- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java
+++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/AnnotationTest.java
@@ -45,6 +45,10 @@ import ghidra.framework.store.FileSystem;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.Address;
+import ghidra.program.model.address.AddressFactory;
+import ghidra.program.model.data.UnsignedIntegerDataType;
+import ghidra.program.model.data.VoidDataType;
+import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.mem.Memory;
import ghidra.program.model.symbol.*;
@@ -83,51 +87,55 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
builder.createLabel("1001018", "mySym{0}"); // symbol with braces
builder.createLabel("1001022", "mySym\\{0\\}"); // symbol with braces escaped
+ Function myFunction =
+ builder.createEmptyFunction("MyFunction", "1002000", 0x10, VoidDataType.dataType);
+ builder.createLocalVariable(myFunction, "myVariable", UnsignedIntegerDataType.dataType, 10);
+
return builder.getProgram();
}
@Test
public void testSymbolAnnotationWithAddress() {
String rawComment = "This is a symbol {@sym 01001014} annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals(rawComment, fixed);
// with display string
rawComment = "This is a symbol {@sym 01001014 bob} annotation.";
- fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals(rawComment, fixed);
}
@Test
public void testSymbolAnnotationWithInvalidAddress() {
String rawComment = "This is a symbol {@sym 999999} annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals(rawComment, fixed);
}
@Test
public void testSymbolAnnotationWithSymbol() {
String rawComment = "This is a symbol {@sym LAB_01003d2c} annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("This is a symbol {@sym 01003d2c} annotation.", fixed);
// with display string
rawComment = "This is a symbol {@sym LAB_01003d2c displayText} annotation.";
- fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("This is a symbol {@sym 01003d2c displayText} annotation.", fixed);
}
@Test
public void testSymbolAnnotationWithInvalidSymbol() {
String rawComment = "This is a symbol {@sym CocoPebbles} annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("This is a symbol {@sym CocoPebbles} annotation.", fixed);
}
@Test
public void testNoAnnotation() {
String rawComment = "This is no symbol annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals(rawComment, fixed);
}
@@ -135,7 +143,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
public void testMixedAnnotationNoSymbolAnnotation() {
String rawComment = "This is a symbol {@url www.noplace.com} annotation " +
"with a {@program notepad} annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals(rawComment, fixed);
}
@@ -143,7 +151,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
public void testMixedAnnotationWithSymbolAnnotation() {
String rawComment = "This is a symbol {@sym LAB_01003d2c} annotation " +
"with a {@program notepad} annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("This is a symbol {@sym 01003d2c} annotation " +
"with a {@program notepad} annotation.", fixed);
}
@@ -151,21 +159,21 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testSymbolAnnotationAtBeginningOfComment() {
String rawComment = "{@sym LAB_01003d2c} annotation at the beginning.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("{@sym 01003d2c} annotation at the beginning.", fixed);
}
@Test
public void testSymbolAnnotation_BackToBack() {
String rawComment = "Test {@sym LAB_01003d2c}{@sym LAB_01003d2c} end.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("Test {@sym 01003d2c}{@sym 01003d2c} end.", fixed);
}
@Test
public void testSymbolAnnotationAtEndOfComment() {
String rawComment = "Annotation at the end {@sym LAB_01003d2c}";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("Annotation at the end {@sym 01003d2c}", fixed);
}
@@ -173,7 +181,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
public void testSymbolAnnotationAtBeginningAndEndOfComment() {
String rawComment =
"{@sym LAB_01003d2c} annotation at the beginning and end {@sym LAB_01003d5b}";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("{@sym 01003d2c} annotation at the " + "beginning and end {@sym 01003d5b}",
fixed);
}
@@ -183,7 +191,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
String rawComment =
"{@sym LAB_01003d2c} annotation at the beginning, middle {@sym LAB_01003d28} and " +
"end {@sym LAB_01003d5b}";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("{@sym 01003d2c} annotation at the beginning, middle {@sym 01003d28} and " +
"end {@sym 01003d5b}", fixed);
}
@@ -192,7 +200,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
public void testSymbolAnnotationWithValidAndInvalidSymbol() {
String rawComment = "This is a symbol {@sym LAB_01003d2c} annotation " +
"with a {@sym FruityPebbles} annotation.";
- String fixed = CommentUtils.fixupAnnotations(rawComment, program);
+ String fixed = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("This is a symbol {@sym 01003d2c} annotation " +
"with a {@sym FruityPebbles} annotation.", fixed);
}
@@ -733,6 +741,52 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
// Navigation performed by ProgramManager not tested due to use of spyServiceProvider
}
+ @Test
+ public void testVariableAnnotation_Basic() {
+ String rawComment = "{@variable Stack[0xa] MyFunction}";
+ String display = CommentUtils.getDisplayString(rawComment, program);
+ assertEquals("myVariable", display);
+ }
+
+ @Test
+ public void testVariableAnnotation_BasicModify() {
+ String rawComment = "{@variable myVariable MyFunction}";
+ String display = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
+ assertEquals("{@variable Stack[0xa] 01002000}", display);
+ }
+
+ @Test
+ public void testVariableAnnotation_BasicModify_NoFunction() {
+ String rawComment = "{@variable myVariable}";
+ String functionAddress = "01002000";
+ Address entryPoint = addr(functionAddress);
+ String display = CommentUtils.fixupAnnotations(rawComment, program, entryPoint);
+ assertEquals("{@variable Stack[0xa] 01002000}", display);
+ }
+
+ @Test
+ public void testLocalAnnotation_UserMarked() {
+ String rawComment = "{@variable Stack[0xa] MyFunction}";
+ String display = CommentUtils.getDisplayString(rawComment, program);
+ assertEquals("myVariable", display);
+ }
+
+ @Test
+ public void testLocalAnnotation_UserMarkedModify() {
+ String rawComment = "{@variable Stack[0xa] MyFunction}";
+ String display = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
+ assertEquals("{@variable Stack[0xa] 01002000}", display);
+ }
+
+ @Test
+ public void testLocalAnnotation_UserMarkedModify_NoFunction() {
+ String rawComment = "{@variable Stack[0xa]}";
+ String functionAddress = "01002000";
+ Address entryPoint = addr(functionAddress);
+ String display = CommentUtils.fixupAnnotations(rawComment, program, entryPoint);
+ assertEquals("{@variable Stack[0xa] 01002000}", display);
+ }
+
@Test
public void testUnknownAnnotation() {
String rawComment = "This is a symbol {@syyyybol bob} annotation";
@@ -917,6 +971,10 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
waitForSwing(); // let post-dialog processing happen
}
+ private Address addr(String offset) {
+ AddressFactory af = program.getAddressFactory();
+ return af.getAddress(offset);
+ }
//==================================================================================================
// Fake/Spy Classes
//==================================================================================================
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/VariableDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/VariableDB.java
index 5de1fc04b7..018c0b0217 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/VariableDB.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/function/VariableDB.java
@@ -16,8 +16,7 @@
package ghidra.program.database.function;
import java.util.List;
-
-import org.apache.commons.lang3.StringUtils;
+import java.util.Objects;
import ghidra.program.database.data.DataTypeUtilities;
import ghidra.program.database.symbol.SymbolDB;
@@ -353,14 +352,13 @@ public abstract class VariableDB implements Variable {
}
Variable otherVar = (Variable) obj;
-
if (!isEquivalent(otherVar)) {
return false;
}
- if (!StringUtils.equals(getName(), otherVar.getName())) {
+ if (!Objects.equals(getName(), otherVar.getName())) {
return false;
}
- return StringUtils.equals(getComment(), otherVar.getComment());
+ return Objects.equals(getComment(), otherVar.getComment());
}
@Override