Annotations: Variable handler

This commit is contained in:
hay-mon
2026-02-13 15:24:19 +00:00
committed by dragonmacher
parent 510c4d14c7
commit f07b8fab84
7 changed files with 352 additions and 30 deletions
@@ -104,11 +104,6 @@
Notes:
<ul style="margin-left: 10px;">
<li>
If you provide a symbol name and not an address, then you will need to
fully-qualify the symbol name if it is not in the global namespace (e.g.,
FunctionFoo::param1 to link to a specific function parameter).
</li>
<li>
If you specify a symbol name instead of an address, then the text of the comment
will be changed to use the address of that symbol.
@@ -285,6 +280,58 @@
</TD>
</TR>
<TR>
<TD valign="top" width="5%">Variable<BR>
</TD>
<TD valign="top" width="15%">Displays the text of the given variable or parameter
for a given function as a hyperlink.<BR>
<BR>
<BR>
Notes:
<ul style="margin-left: 10px;">
<li>
This annotation will modify itself into the @var_hash format with a function
address to survive renaming of the variable and the function.
</li>
</ul>
</TD>
<TD valign="top" width="12%">
<OL style="margin-left: 15px;">
<LI>variable/parameter name (@var) or hash identifier (@var_hash)</LI>
<LI>[function name or address]<BR>
</LI>
</OL>
</TD>
<TD valign="top" width="10%">
<UL style="margin-left: 10px;">
<LI>@variable</LI>
<LI>@var</LI>
<LI>@var_hash</LI>
</UL>
</TD>
<TD valign="top" width="25%">
<UL style="margin-left: 10px;">
<LI>{@variable "i"}</LI>
<LI>{@var "argv" "main"}</LI>
<LI>{@var_hash 02a2a2a "main"}</LI>
<LI>{@var_hash 0101010 "1000002a"}<BR>
</LI>
</UL>
</TD>
</TR>
<TR>
<TD valign="top" width="5%"><I>Discovered Annotations</I><BR>
</TD>
@@ -83,29 +83,30 @@ public class SetCommentsCmd implements Command<Program> {
CodeUnit cu = getCodeUnit(program);
if (cu != null) {
Address loc = cu.getAddress();
if (commentChanged(cu.getComment(CommentType.PRE), preComment)) {
String updatedPreComment = CommentUtils.fixupAnnotations(preComment, program);
String updatedPreComment = CommentUtils.fixupAnnotations(preComment, program, loc);
updatedPreComment = CommentUtils.sanitize(updatedPreComment);
cu.setComment(CommentType.PRE, updatedPreComment);
}
if (commentChanged(cu.getComment(CommentType.POST), postComment)) {
String updatedPostComment = CommentUtils.fixupAnnotations(postComment, program);
String updatedPostComment = CommentUtils.fixupAnnotations(postComment, program, loc);
updatedPostComment = CommentUtils.sanitize(updatedPostComment);
cu.setComment(CommentType.POST, updatedPostComment);
}
if (commentChanged(cu.getComment(CommentType.EOL), eolComment)) {
String updatedEOLComment = CommentUtils.fixupAnnotations(eolComment, program);
String updatedEOLComment = CommentUtils.fixupAnnotations(eolComment, program, loc);
updatedEOLComment = CommentUtils.sanitize(updatedEOLComment);
cu.setComment(CommentType.EOL, updatedEOLComment);
}
if (commentChanged(cu.getComment(CommentType.PLATE), plateComment)) {
String updatedPlateComment = CommentUtils.fixupAnnotations(plateComment, program);
String updatedPlateComment = CommentUtils.fixupAnnotations(plateComment, program, loc);
updatedPlateComment = CommentUtils.sanitize(updatedPlateComment);
cu.setComment(CommentType.PLATE, updatedPlateComment);
}
if (commentChanged(cu.getComment(CommentType.REPEATABLE), repeatableComment)) {
String updatedRepeatableComment =
CommentUtils.fixupAnnotations(repeatableComment, program);
CommentUtils.fixupAnnotations(repeatableComment, program, loc);
updatedRepeatableComment = CommentUtils.sanitize(updatedRepeatableComment);
cu.setComment(CommentType.REPEATABLE, updatedRepeatableComment);
}
@@ -22,6 +22,7 @@ import docking.widgets.fieldpanel.field.AttributedString;
import docking.widgets.fieldpanel.field.FieldElement;
import ghidra.app.nav.Navigatable;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.util.bean.field.AnnotatedTextFieldElement;
@@ -124,6 +125,11 @@ public interface AnnotatedStringHandler extends ExtensionPoint {
default String getPrototypeString(String displayText) {
return getPrototypeString();
}
@Deprecated(forRemoval = true)
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
@@ -132,9 +138,10 @@ public interface AnnotatedStringHandler extends ExtensionPoint {
*
* @param text the array of annotation parts to modify
* @param program the program
* @param loc location of the annotation in the program
* @return the modified array; null otherwise
*/
default String[] modify(String[] text, Program program) {
default String[] modify(String[] text, Program program, Address loc) {
return null;
}
@@ -50,9 +50,10 @@ public class CommentUtils {
*
* @param rawCommentText the text to be updated
* @param program the program associated with the comment
* @param loc location 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 loc) {
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, loc);
if (updatedParts == null) {
return annotation; // nothing to change
}
@@ -126,7 +126,7 @@ public class SymbolAnnotatedStringHandler implements AnnotatedStringHandler {
}
@Override
public String[] modify(String[] text, Program program) {
public String[] modify(String[] text, Program program, Address loc) {
if (text.length <= 1) {
return null;
}
@@ -0,0 +1,217 @@
/* ###
* 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.util.viewer.field;
import docking.widgets.fieldpanel.field.AttributedString;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.nav.Navigatable;
import ghidra.app.services.GoToService;
import ghidra.app.util.NamespaceUtils;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.Symbol;
/**
* An annotated string handler that handles annotations that begin with
* {@link #SUPPORTED_ANNOTATIONS}. This class expects one string following the annotation
* text that is the name of a script.
*/
public class VariableAnnotatedStringHandler implements AnnotatedStringHandler {
private static final String DEFAULT_ANO = "var";
private static final String HASH_ANO = DEFAULT_ANO + "_hash";
private static final String[] SUPPORTED_ANNOTATIONS = { "variable", DEFAULT_ANO, HASH_ANO };
private static final String INVALID_SYMBOL_TEXT =
"@" + DEFAULT_ANO + " annotation must have form: <var_sym> [<func_sym>]";
@Override
public AttributedString createAnnotatedString(AttributedString prototypeString, String[] text,
Program program) {
if (program == null) { // this can happen during merge operations
final StringBuilder buffer = new StringBuilder();
for (String string : text) {
buffer.append(string).append(" ");
}
return new AttributedString(buffer.toString(), Palette.LIGHT_GRAY,
prototypeString.getFontMetrics(0));
}
if (text.length != 3) {
if (text.length == 2) {
throw new AnnotationException("Function symbol is not optional when rendering.");
}
throw new AnnotationException(INVALID_SYMBOL_TEXT);
}
final Function func = getFunction(program, text[2]);
if (func == null) {
throw new AnnotationException("Could not find function matching \"" + text[2] + "\"");
}
final Variable var = getVariable(func, getFilterGenerator(text[0]).apply(text[1]));
if (var == null) {
throw new AnnotationException("Could not find variable in function \"" +
func.getName() + "\" matching \"" + text[1] + "\".");
}
return new AttributedString(var.getName(), prototypeString.getColor(0),
prototypeString.getFontMetrics(0), true, prototypeString.getColor(0));
}
@Override
public String[] modify(String[] text, Program program, Address loc) {
if (program == null) { // this can happen during merge operations
return null;
}
Function func = null;
switch (text.length) {
case 3:
func = getFunction(program, text[2]);
break;
case 2:
func = program.getFunctionManager().getFunctionContaining(loc);
break;
default:
return null;
}
if (func == null) {
return null;
}
final Variable var = getVariable(func, getFilterGenerator(text[0]).apply(text[1]));
if (var == null) {
return null;
}
return new String[] {
HASH_ANO,
Integer.toUnsignedString(var.hashCode(), 16),
func.getEntryPoint().toString()
};
}
@Override
public String[] getSupportedAnnotations() {
return SUPPORTED_ANNOTATIONS;
}
@Override
public boolean handleMouseClick(String[] annotationParts, Navigatable sourceNavigatable,
ServiceProvider serviceProvider) {
final Program program = sourceNavigatable.getProgram();
if (annotationParts.length != 3) {
return false;
}
final Function func = getFunction(program, annotationParts[2]);
if (func == null) {
return false;
}
final Variable var = getVariable(func, getFilterGenerator(annotationParts[0]).apply(annotationParts[1]));
if (var == null) {
return false;
}
Symbol sym = var.getSymbol();
if (sym == null) {
sym = func.getSymbol();
}
final GoToService goToService = serviceProvider.getService(GoToService.class);
return goToService.goTo(sym.getProgramLocation());
}
@Override
public String getDisplayString() {
return "Variable";
}
@Override
public String getPrototypeString() {
return "{@" + DEFAULT_ANO + " var_sym [func_sym]}";
}
@Override
public String getPrototypeString(String displayText) {
return "{@" + DEFAULT_ANO + " " + displayText.trim() + "}";
}
private static Function getFunction(final Program program, final String name) {
final FunctionManager func_manager = program.getFunctionManager();
for (Symbol sym : NamespaceUtils.getSymbols(name, program)) {
final Function func = func_manager.getFunctionAt(sym.getAddress());
if (func != null) {
return func;
}
}
// if we get here, then see if the value is an address
final Address addr = program.getAddressFactory().getAddress(name);
if (addr != null) {
final Function func = func_manager.getFunctionAt(addr);
if (func != null) {
return func;
}
}
return null;
}
private static Variable getVariable(final Function func, JFunction<Variable, Boolean> name_filter) {
for (Variable var : func.getAllVariables()) {
if (name_filter.apply(var)) {
return var;
}
}
return null;
}
/**
* Get the correct filter generator for parsing the local variable.
*
* @param name the name of the annotation
* @return the filter generator
*/
private static JFunction<String,JFunction<Variable, Boolean>> getFilterGenerator(final String name) {
switch (name) {
case HASH_ANO:
return VariableAnnotatedStringHandler::hashFilterGen;
default:
return VariableAnnotatedStringHandler::nameFilterGen;
}
}
private static JFunction<Variable, Boolean> hashFilterGen(String s) {
try {
final int hash = Integer.parseUnsignedInt(s, 16);
return (x) -> x.hashCode() == hash;
} catch (NumberFormatException e) {
return (x) -> false;
}
}
private static JFunction<Variable, Boolean> nameFilterGen(String s) {
return (x) -> x.getName().equals(s);
}
private interface JFunction<T, R> extends java.util.function.Function<T, R> {}
}
@@ -15,7 +15,7 @@
*/
package ghidra.app.util.viewer.field;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.*;
@@ -45,6 +45,9 @@ 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.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.*;
@@ -61,6 +64,7 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
private static final String OTHER_PROGRAM_NAME = "program2";
private Program program;
private Function test_func;
@Before
public void setUp() throws Exception {
@@ -83,51 +87,54 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
builder.createLabel("1001018", "mySym{0}"); // symbol with braces
builder.createLabel("1001022", "mySym\\{0\\}"); // symbol with braces escaped
test_func = builder.createEmptyFunction("test_func", "1002000", 0x10, VoidDataType.dataType);
builder.createLocalVariable(test_func, "test_var", 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 +142,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 +150,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 +158,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 +180,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 +190,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 +199,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 +740,48 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
// Navigation performed by ProgramManager not tested due to use of spyServiceProvider
}
@Test
public void testVariableAnnotation_Basic() {
String rawComment = "{@var test_var test_func}";
String display = CommentUtils.getDisplayString(rawComment, program);
assertEquals("test_var", display);
}
@Test
public void testVariableAnnotation_BasicModify() {
String rawComment = "{@var test_var test_func}";
String display = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("{@var_hash a016221 01002000}", display);
}
@Test
public void testVariableAnnotation_BasicModify_NoFunction() {
String rawComment = "{@var test_var}";
String display = CommentUtils.fixupAnnotations(rawComment, program, test_func.getEntryPoint());
assertEquals("{@var_hash a016221 01002000}", display);
}
@Test
public void testLocalAnnotation_UserMarked() {
String rawComment = "{@var_hash a016221 test_func}";
String display = CommentUtils.getDisplayString(rawComment, program);
assertEquals("test_var", display);
}
@Test
public void testLocalAnnotation_UserMarkedModify() {
String rawComment = "{@var_hash a016221 test_func}";
String display = CommentUtils.fixupAnnotations(rawComment, program, Address.NO_ADDRESS);
assertEquals("{@var_hash a016221 01002000}", display);
}
@Test
public void testLocalAnnotation_UserMarkedModify_NoFunction() {
String rawComment = "{@var_hash a016221}";
String display = CommentUtils.fixupAnnotations(rawComment, program, test_func.getEntryPoint());
assertEquals("{@var_hash a016221 01002000}", display);
}
@Test
public void testUnknownAnnotation() {
String rawComment = "This is a symbol {@syyyybol bob} annotation";