diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/EolComments.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/EolComments.java index 6e49c524b0..0b8075c3aa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/EolComments.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/EolComments.java @@ -40,6 +40,28 @@ import util.CollectionUtils; * Repeatable comment for the code unit, any repeatable comments for the code units that this code * unit has references to, and possibly a comment indicating the data at a code unit that is * referenced by this code unit. + * + * This section describes the various EOL comment types that may appear. The user can + * toggle which types are enabled. The comments are displayed in the order listed below. + * + * EOL Types: + * + * - EOL - user end of line comment + * + * - Repeatable - user repeatable source comment *at the code unit* + * - Ref Repeatable- for every reference *from a code unit*, show the target: + * - address repeatable, + * - function repeatable, + * - code unit repeatable + * + * - Auto - fabricated reference preview: + * - function, + * - indirect data pointer, + * - direct data access preview + * *depending on the options, this typically do not appear when + * repeatable comments exist + * + * - Offcut - comments at addresses inside of a code unit */ public class EolComments { @@ -67,6 +89,7 @@ public class EolComments { this.operandsShowReferences = operandsShowReferences; this.maxDisplayComments = maxDisplayComments; this.extraCommentsOption = extraCommentsOption; + loadComments(); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java index 1370f3b2a2..7b545eb6ec 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/EolCommentFieldFactory.java @@ -17,8 +17,8 @@ package ghidra.app.util.viewer.field; import java.awt.Color; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.function.Consumer; import docking.widgets.fieldpanel.field.*; import docking.widgets.fieldpanel.support.*; @@ -26,6 +26,7 @@ import generic.theme.GThemeDefaults.Colors.Palette; import ghidra.app.util.*; import ghidra.app.util.viewer.field.ListingColors.CommentColors; import ghidra.app.util.viewer.format.FieldFormatModel; +import ghidra.app.util.viewer.listingpanel.ListingModel; import ghidra.app.util.viewer.options.OptionsGui; import ghidra.app.util.viewer.proxy.ProxyObj; import ghidra.framework.options.*; @@ -33,8 +34,10 @@ import ghidra.program.model.address.Address; import ghidra.program.model.listing.*; import ghidra.program.util.*; import ghidra.util.HelpLocation; +import ghidra.util.StringUtilities; import ghidra.util.bean.field.AnnotatedTextFieldElement; import ghidra.util.exception.AssertException; +import utility.function.Dummy; /** * Generates End of line comment Fields. @@ -236,6 +239,7 @@ public class EolCommentFieldFactory extends FieldFactory { if (!enabled || !(obj instanceof CodeUnit)) { return null; } + CodeUnit cu = (CodeUnit) obj; Program program = cu.getProgram(); @@ -244,14 +248,8 @@ public class EolCommentFieldFactory extends FieldFactory { // comments if open. If this was allowed, then the comment would appear // on the outside data container and on the 1st internal member // - if (cu instanceof Data) { - Data data = (Data) cu; - if (data.getNumComponents() > 0) { - boolean isOpen = proxy.getListingLayoutModel().isOpen((Data) proxy.getObject()); - if (isOpen) { - return null; // avoid double showing - } - } + if (isOpenData(cu, proxy)) { + return null; } EolComments comments = new EolComments(cu, codeUnitFormatOptions.followReferencedPointers(), @@ -261,25 +259,59 @@ public class EolCommentFieldFactory extends FieldFactory { List elementList = new ArrayList<>(); AttributedString prefix = createPrefix(CommentStyle.EOL); List eols = comments.getEOLComments(); - List eolElements = convertToFieldElements(program, eols, prefix, 0); + List eolElements = convertToFieldElements(program, eols, prefix, 0, true); elementList.addAll(eolElements); + /* + This section describes the various EOL comment types that may appear. The user can + toggle which types are enabled. The comments are displayed in the order listed below. + + EOL Types: + + - EOL - user end of line comment + + - Repeatable - user repeatable source comment *at the code unit* + - Ref Repeatable- for every reference *from a code unit*, show the target: + - address repeatable, + - function repeatable, + - code unit repeatable + + - Auto - fabricated reference preview: + - function, + - indirect data pointer, + - direct data access preview + *depending on the options, this typically do not appear when + repeatable comments exist + + - Offcut - comments at addresses inside of a code unit + */ + if (comments.isShowingRepeatables()) { prefix = createPrefix(CommentStyle.REPEATABLE); int row = getNextRow(elementList); List repeatables = comments.getRepeatableComments(); - List elements = convertToFieldElements(program, repeatables, prefix, row); + List elements = + convertToFieldElements(program, repeatables, prefix, row, true); elementList.addAll(elements); } if (comments.isShowingRefRepeatables()) { - prefix = createPrefix(CommentStyle.REF_REPEATABLE); + + AttributedString refPrefix = createPrefix(CommentStyle.REF_REPEATABLE); List refRepeatables = comments.getReferencedRepeatableComments(); for (RefRepeatComment comment : refRepeatables) { + int row = getNextRow(elementList); String[] lines = comment.getCommentLines(); + Address refAddress = comment.getAddress(); + List linesList = Arrays.asList(lines); + + Consumer> decorator = elements -> { + prependRefAddress(program, refPrefix, refAddress, elements); + }; + List elements = - convertToRefFieldElements(lines, program, prefix, comment.getAddress(), row); + convertToFieldElements(program, linesList, decorator, prefix, row, true); elementList.addAll(elements); } } @@ -288,7 +320,11 @@ public class EolCommentFieldFactory extends FieldFactory { prefix = createPrefix(CommentStyle.AUTO); int row = getNextRow(elementList); List autos = comments.getAutomaticComment(); - List elements = convertToFieldElements(program, autos, prefix, row); + + // Note: we pass 'false' for allowing annotations so that the user will see the raw data + // and not an interpreted annotation. + List elements = + convertToFieldElements(program, autos, prefix, row, false); elementList.addAll(elements); } @@ -296,7 +332,8 @@ public class EolCommentFieldFactory extends FieldFactory { prefix = createPrefix(CommentStyle.OFFCUT); int row = getNextRow(elementList); List offcuts = comments.getOffcutEolComments(); - List elements = convertToFieldElements(program, offcuts, prefix, row); + List elements = + convertToFieldElements(program, offcuts, prefix, row, true); elementList.addAll(elements); } @@ -307,6 +344,18 @@ public class EolCommentFieldFactory extends FieldFactory { maxDisplayLines, hlProvider); } + private boolean isOpenData(CodeUnit cu, ProxyObj proxy) { + if (cu instanceof Data data) { + if (data.getNumComponents() > 0) { + ListingModel listingModel = proxy.getListingLayoutModel(); + if (listingModel.isOpen(data)) { + return true; + } + } + } + return false; + } + private AttributedString createPrefix(CommentStyle commentStyle) { if (commentStyle == CommentStyle.EOL) { return new AttributedString(SEMICOLON_PREFIX, CommentColors.EOL, getMetrics(style), @@ -349,7 +398,15 @@ public class EolCommentFieldFactory extends FieldFactory { } private List convertToFieldElements(Program program, List comments, - AttributedString prefix, int row) { + AttributedString prefix, int row, boolean allowAnnotations) { + + Consumer> decorator = Dummy.consumer(); // no decorations by default + return convertToFieldElements(program, comments, decorator, prefix, row, allowAnnotations); + } + + private List convertToFieldElements(Program program, List comments, + Consumer> decorator, AttributedString prefix, int row, + boolean allowAnnotations) { List fieldElements = new ArrayList<>(); if (comments.isEmpty()) { @@ -357,11 +414,15 @@ public class EolCommentFieldFactory extends FieldFactory { } for (int commentRow = 0; commentRow < comments.size(); commentRow++) { - int offsetRow = row + commentRow; - fieldElements.add(CommentUtils.parseTextForAnnotations(comments.get(commentRow), - program, prefix, offsetRow)); + int encodedRow = row + commentRow; + String commentText = comments.get(commentRow); + FieldElement element = + createCommentField(commentText, prefix, program, encodedRow, allowAnnotations); + fieldElements.add(element); } + decorator.accept(fieldElements); + if (isWordWrap) { int lineWidth = showSemicolon ? width - prefix.getStringWidth() : width; fieldElements = FieldUtils.wrap(fieldElements, lineWidth); @@ -381,57 +442,43 @@ public class EolCommentFieldFactory extends FieldFactory { return fieldElements; } - private List convertToRefFieldElements(String[] comments, Program program, - AttributedString currentPrefixString, Address refAddress, int nextRow) { + private FieldElement createCommentField(String commentText, AttributedString prototype, + Program program, int row, boolean allowAnnotations) { - int numCommentLines = comments.length; - List fieldElements = new ArrayList<>(); - if (numCommentLines == 0) { - return fieldElements; - } - for (int rowIndex = 0; rowIndex < numCommentLines; rowIndex++) { - int encodedRow = nextRow + rowIndex; - fieldElements.add(CommentUtils.parseTextForAnnotations(comments[rowIndex], program, - currentPrefixString, encodedRow)); - } - if (prependRefAddress) { - FieldElement commentElement = fieldElements.get(0); - // Address - String refAddrComment = "{@address " + refAddress.toString() + "}"; - RowColLocation startRowCol = commentElement.getDataLocationForCharacterIndex(0); - int encodedRow = startRowCol.row(); - int encodedCol = startRowCol.col(); - Annotation annotation = new Annotation(refAddrComment, program); - FieldElement addressElement = - new AnnotatedTextFieldElement(annotation, currentPrefixString, program, encodedRow, - encodedCol); - - // Space character - AttributedString spaceStr = new AttributedString(" ", currentPrefixString.getColor(0), - currentPrefixString.getFontMetrics(0), false, null); - FieldElement spacerElement = new TextFieldElement(spaceStr, encodedRow, encodedCol); - fieldElements.add(new CompositeFieldElement( - new FieldElement[] { addressElement, spacerElement, commentElement })); + if (allowAnnotations) { + return CommentUtils.parseTextForAnnotations(commentText, program, prototype, row); } - if (isWordWrap) { - int lineWidth = showSemicolon ? width - currentPrefixString.getStringWidth() : width; - fieldElements = FieldUtils.wrap(fieldElements, lineWidth); + String text = StringUtilities.convertTabsToSpaces(commentText); + AttributedString as = new AttributedString(text, prototype.getColor(0), + prototype.getFontMetrics(0), false, null); + return new TextFieldElement(as, row, 0); + } + + private void prependRefAddress(Program program, AttributedString prefix, Address refAddress, + List fieldElements) { + + if (!prependRefAddress) { + return; } - if (showSemicolon) { - for (int i = 0; i < fieldElements.size(); i++) { - RowColLocation startRowCol = - fieldElements.get(i).getDataLocationForCharacterIndex(0); - int encodedRow = startRowCol.row(); - int encodedCol = startRowCol.col(); - FieldElement prefixFieldElement = - new TextFieldElement(currentPrefixString, encodedRow, encodedCol); - fieldElements.set(i, new CompositeFieldElement( - new FieldElement[] { prefixFieldElement, fieldElements.get(i) })); - } - } - return fieldElements; + FieldElement commentElement = fieldElements.get(0); + + // Address + String refAddrComment = "{@address " + refAddress.toString() + "}"; + RowColLocation startRowCol = commentElement.getDataLocationForCharacterIndex(0); + int encodedRow = startRowCol.row(); + int encodedCol = startRowCol.col(); + Annotation annotation = new Annotation(refAddrComment, program); + FieldElement addressElement = + new AnnotatedTextFieldElement(annotation, prefix, program, encodedRow, encodedCol); + + // Space character + AttributedString spaceStr = new AttributedString(" ", prefix.getColor(0), + prefix.getFontMetrics(0), false, null); + FieldElement spacerElement = new TextFieldElement(spaceStr, encodedRow, encodedCol); + fieldElements.set(0, new CompositeFieldElement( + new FieldElement[] { addressElement, spacerElement, commentElement })); } /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java b/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java index 23add48ab5..d14cf1f851 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/program/database/ProgramBuilder.java @@ -866,6 +866,18 @@ public class ProgramBuilder { } } + /** + * Creates a non-null-terminated ascii string at the given address + * @param address the address + * @param string the string + * @return the new data + * @throws Exception if there is an exception + */ + public Data createString(String address, String string) throws Exception { + return createString(address, string, StandardCharsets.US_ASCII, false, + StringDataType.dataType); + } + public Data createString(String address, String string, Charset charset, boolean nullTerminate, DataType dataType) throws Exception { if (nullTerminate) { diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java index 0bd1e37717..3818e4f7be 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/EolCommentFieldFactoryTest.java @@ -15,6 +15,8 @@ */ package ghidra.app.util.viewer.field; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.StringContains.*; import static org.hamcrest.core.StringStartsWith.*; import static org.junit.Assert.*; @@ -29,10 +31,17 @@ import ghidra.program.database.ProgramDB; import ghidra.program.model.address.Address; import ghidra.program.model.address.AddressFactory; import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; import ghidra.test.*; public class EolCommentFieldFactoryTest extends AbstractGhidraHeadedIntegrationTest { + private static final String STRING_ADDRESS_WITH_ANNOTATION = "0x01002c98"; + private static final String ADDRESS_CALLING_STRING_WITH_ANNOTATION = "0X01002d37"; + private static final String AUTO_COMMENT_TEXT_WITH_ANNOTATION = + "Annotation: {@address 12345678 foo}"; + private TestEnv env; private CodeBrowserPlugin cb; private Options fieldOptions; @@ -75,21 +84,45 @@ public class EolCommentFieldFactoryTest extends AbstractGhidraHeadedIntegrationT public void testRepeatableComment_FunctionCall() throws Exception { // check existing auto comment - ListingTextField tf = getFieldText(addr("0x010022e6")); + String from = "0x010022e6"; + ListingTextField tf = getFieldText(addr(from)); assertEquals(1, tf.getNumRows()); assertThat(tf.getText(), startsWith("undefined ghidra(undefined4 param_1,")); - // set repeatable comment at destination - Address destination = addr("0x01002cf5"); + // set repeatable comment at source + String to = "0x01002cf5"; String repeatableComment = "My repeatable comment"; - setRepeatableComment(destination, repeatableComment); + setRepeatableComment(addr(to), repeatableComment); - // check that the auto comment now matches the updated comment - tf = getFieldText(addr("0x010022e6")); + // check that the repeatable comment now matches the updated comment + tf = getFieldText(addr(from)); assertEquals(1, tf.getNumRows()); assertEquals(tf.getText(), repeatableComment); } + @Test + public void testRepeatableComment_FunctionCall_PrependRefAddress() throws Exception { + + setBooleanOption(EolCommentFieldFactory.ENABLE_PREPEND_REF_ADDRESS_KEY, true); + + // check existing auto comment + String from = "0x010022e6"; + ListingTextField tf = getFieldText(addr(from)); + assertEquals(1, tf.getNumRows()); + assertThat(tf.getText(), startsWith("undefined ghidra(undefined4 param_1,")); + + // set repeatable comment at source + String to = "0x01002cf5"; + String repeatableComment = "My repeatable comment"; + setRepeatableComment(addr(to), repeatableComment); + + // check that the repeatable comment now matches the updated comment and has the ref address + // prepended + tf = getFieldText(addr(from)); + assertEquals(1, tf.getNumRows()); + assertEquals("01002cf5 " + repeatableComment, tf.getText()); + } + @Test public void testRepeatableComment_DataAccess() throws Exception { @@ -106,7 +139,21 @@ public class EolCommentFieldFactoryTest extends AbstractGhidraHeadedIntegrationT // check that the auto comment now matches the updated comment tf = getFieldText(addr("0x01002265")); assertEquals(1, tf.getNumRows()); - assertEquals(tf.getText(), repeatableComment); + assertEquals(repeatableComment, tf.getText()); + } + + @Test + public void testAutoCommentDoesNotRenderAnnotation() { + + /* + Creates a data reference to a string containing an annotation. Tests that the + annotation is not rendered, but is shown in its raw form. + */ + + goTo(env.getTool(), program, STRING_ADDRESS_WITH_ANNOTATION); + ListingTextField tf = getFieldText(addr(ADDRESS_CALLING_STRING_WITH_ANNOTATION)); + assertEquals(1, tf.getNumRows()); + assertThat(tf.getText(), containsString(AUTO_COMMENT_TEXT_WITH_ANNOTATION)); } //================================================================================================== @@ -115,6 +162,13 @@ public class EolCommentFieldFactoryTest extends AbstractGhidraHeadedIntegrationT private ProgramDB buildProgram() throws Exception { ClassicSampleX86ProgramBuilder builder = new ClassicSampleX86ProgramBuilder(); + + builder.createString(STRING_ADDRESS_WITH_ANNOTATION, AUTO_COMMENT_TEXT_WITH_ANNOTATION); + + builder.createMemoryReference(ADDRESS_CALLING_STRING_WITH_ANNOTATION, + STRING_ADDRESS_WITH_ANNOTATION, + RefType.DATA, SourceType.ANALYSIS); + return builder.getProgram(); }