diff --git a/Ghidra/Features/Base/data/base.listing.theme.properties b/Ghidra/Features/Base/data/base.listing.theme.properties index ff03aea7e4..8d58d46fbb 100644 --- a/Ghidra/Features/Base/data/base.listing.theme.properties +++ b/Ghidra/Features/Base/data/base.listing.theme.properties @@ -37,6 +37,7 @@ color.fg.listing.comment.ref.repeatable = color.palette.cornflowerblue color.fg.listing.comment.plate = color.palette.gray color.fg.listing.comment.post = color.palette.blue color.fg.listing.comment.pre = color.palette.indigo +color.fg.listing.comment.offcut = color.fg.error color.fg.listing.ref.bad = color.fg.error color.fg.listing.ext.entrypoint = color.palette.magenta color.fg.listing.ext.ref.resolved = color.palette.darkorange diff --git a/Ghidra/Features/Base/src/main/help/help/topics/CommentsPlugin/Comments.htm b/Ghidra/Features/Base/src/main/help/help/topics/CommentsPlugin/Comments.htm index 8c9dba9a83..d32d08c493 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/CommentsPlugin/Comments.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/CommentsPlugin/Comments.htm @@ -73,6 +73,15 @@ annotations to change the display characteristics of data entered into the comment fields.

+

A comment can also be offcut. This means it is + defined on an address that is in the middle of an instruction or data item. In this case, + the offcut comment will be displayed in the appropriate field (EOL, PLATE, POST, PRE) on the + instruction or data that contains that address and will be colored red to indicate the comment + has been attached to an invalid address. Note that this comment can only be edited if the + containing instruction or data is first cleared so that the individual addresses are shown in + the listing. +

+

Adding or Editing Comments (Set Comment)

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 3fadd068ec..27f3c5f16c 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 @@ -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. @@ -20,8 +20,7 @@ import java.util.*; import org.apache.commons.lang3.StringUtils; import docking.widgets.fieldpanel.support.RowColLocation; -import ghidra.app.util.viewer.field.EolEnablement; -import ghidra.app.util.viewer.field.EolExtraCommentsOption; +import ghidra.app.util.viewer.field.*; import ghidra.program.model.address.*; import ghidra.program.model.data.*; import ghidra.program.model.listing.*; @@ -53,6 +52,7 @@ public class EolComments { private List refRepeatables = new ArrayList<>(); private List autos = new ArrayList<>(); private List references = new ArrayList<>(); + private List offcuts = new ArrayList<>(); // used to signal the operand is already displaying a pointer reference, so there is no need for // this class to create a comment to do the same @@ -75,6 +75,7 @@ public class EolComments { loadRepeatables(); loadRefRepeatables(); loadAutos(); + loadOffcutEols(); } /** @@ -101,10 +102,15 @@ public class EolComments { private void loadEols() { Collection comments = - Arrays.asList(codeUnit.getCommentAsArray(CodeUnit.EOL_COMMENT)); + Arrays.asList(codeUnit.getCommentAsArray(CommentType.EOL)); addStrings(comments, eols); } + private void loadOffcutEols() { + Collection comments = CommentUtils.getOffcutComments(codeUnit, CommentType.EOL); + addStrings(comments, offcuts); + } + private void loadRepeatables() { boolean hasOtherComments = !eols.isEmpty(); if (!extraCommentsOption.isShowingRepeatables(hasOtherComments)) { @@ -195,6 +201,10 @@ public class EolComments { return !autos.isEmpty(); } + public boolean isShowingOffcutComments() { + return !offcuts.isEmpty(); + } + private Collection getReferencePreviews() { loadReferences(); @@ -642,6 +652,10 @@ public class EolComments { return Collections.unmodifiableList(autos); } + public List getOffcutEolComments() { + return Collections.unmodifiableList(offcuts); + } + @Override public String toString() { 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 5404a63e5f..e23c86658a 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 @@ -28,8 +28,8 @@ import docking.widgets.fieldpanel.field.*; import generic.theme.GThemeDefaults.Colors; import generic.theme.Gui; import ghidra.app.util.NamespaceUtils; -import ghidra.program.model.address.Address; -import ghidra.program.model.listing.Program; +import ghidra.program.model.address.*; +import ghidra.program.model.listing.*; import ghidra.program.model.symbol.Symbol; import ghidra.program.model.symbol.SymbolTable; import ghidra.util.StringUtilities; @@ -440,4 +440,49 @@ public class CommentUtils { return Collections.unmodifiableSet(getAnnotatedStringHandlerMap().keySet()); } + /** + * Returns a list of offcut comments for the given code unit. All the offcut comments from + * possibly multiple addresses will be combined into a single list of comment lines. + * @param cu the code unit to get offcut comments for + * @param type the type of comment to retrieve (EOL, PRE, PLATE, POST) + * @return a list of all offcut comments for the given code unit. + */ + public static List getOffcutComments(CodeUnit cu, CommentType type) { + // internal data items handle EOL comments, so ignore EOL comments on items that + // have sub-components + if (type == CommentType.EOL && cu instanceof Data data) { + if (data.getNumComponents() > 0) { + return Collections.emptyList(); + } + } + + Address start = cu.getMinAddress().next(); + Address end = cu.getMaxAddress(); + if (start == null || start.compareTo(end) > 0) { + return Collections.emptyList(); + } + + Listing listing = cu.getProgram().getListing(); + AddressSet addrSet = new AddressSet(start, cu.getMaxAddress()); + AddressIterator it = listing.getCommentAddressIterator(type, addrSet, true); + + if (!it.hasNext()) { + return Collections.emptyList(); + } + + List offcutComments = new ArrayList<>(); + + while (it.hasNext()) { + Address next = it.next(); + String comment = listing.getComment(type, next); + if (comment != null) { + String[] lines = StringUtilities.toLines(comment); + for (String line : lines) { + offcutComments.add(line); + } + } + } + return offcutComments; + } + } 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 3925dc3bf9..fa6f4ae3de 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 @@ -64,6 +64,7 @@ public class EolCommentFieldFactory extends FieldFactory { private int repeatableCommentStyle; private int automaticCommentStyle; private int refRepeatableCommentStyle; + private int offcutCommentStyle; private EolExtraCommentsOption extraCommentsOption = new EolExtraCommentsOption(); @@ -292,6 +293,14 @@ public class EolCommentFieldFactory extends FieldFactory { elementList.addAll(elements); } + if (comments.isShowingOffcutComments()) { + prefix = createPrefix(CommentStyle.OFFCUT); + int row = getNextRow(elementList); + List offcuts = comments.getOffcutEolComments(); + List elements = convertToFieldElements(program, offcuts, prefix, row); + elementList.addAll(elements); + } + if (elementList.isEmpty()) { return null; } @@ -316,11 +325,16 @@ public class EolCommentFieldFactory extends FieldFactory { return new AttributedString(SEMICOLON_PREFIX, CommentColors.AUTO, getMetrics(automaticCommentStyle), false, null); } + if (commentStyle == CommentStyle.OFFCUT) { + return new AttributedString(SEMICOLON_PREFIX, CommentColors.OFFCUT, + getMetrics(style), false, null); + } + throw new AssertException("Unexected comment style: " + commentStyle); } private enum CommentStyle { - EOL, REPEATABLE, REF_REPEATABLE, AUTO; + EOL, REPEATABLE, REF_REPEATABLE, AUTO, OFFCUT; } private int getNextRow(List elementList) { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java index 0941c7ac64..b557e55280 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/ListingColors.java @@ -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. @@ -69,6 +69,7 @@ public class ListingColors { public static final GColor PRE= new GColor("color.fg.listing.comment.pre"); public static final GColor REPEATABLE = new GColor("color.fg.listing.comment.repeatable"); public static final GColor REF_REPEATABLE = new GColor("color.fg.listing.comment.ref.repeatable"); + public static final GColor OFFCUT = new GColor("color.fg.listing.comment.offcut"); } public static class LabelColors { diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java index b19be7c7b4..91462a129c 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PlateFieldFactory.java @@ -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. @@ -40,6 +40,7 @@ import ghidra.program.model.listing.*; import ghidra.program.model.symbol.*; import ghidra.program.util.*; import ghidra.util.HelpLocation; +import util.CollectionUtils; /** * Class for showing plate comments @@ -142,12 +143,14 @@ public class PlateFieldFactory extends FieldFactory { CodeUnit cu = (CodeUnit) proxy.getObject(); boolean isClipped = false; List elements = new ArrayList<>(); - String commentText = getCommentText(cu); + List offcutComments = CommentUtils.getOffcutComments(cu, CommentType.PLATE); + String commentText = getCommentText(cu, offcutComments); + if (StringUtils.isBlank(commentText)) { getDefaultFieldElements(cu, elements); } else { - isClipped = getFormattedFieldElements(cu, elements); + isClipped = getFormattedFieldElements(cu, elements, offcutComments); } if (elements.isEmpty()) { @@ -170,14 +173,15 @@ public class PlateFieldFactory extends FieldFactory { return listingField; } - private boolean getFormattedFieldElements(CodeUnit cu, List elements) { + private boolean getFormattedFieldElements(CodeUnit cu, List elements, + List offcutComments) { int numberBlankLines = getNumberBlankLines(cu, true); addBlankLines(elements, numberBlankLines, cu); String[] comments = cu.getCommentAsArray(CodeUnit.PLATE_COMMENT); - return generateFormattedPlateComment(elements, comments, cu.getProgram()); + return generateFormattedPlateComment(elements, comments, offcutComments, cu.getProgram()); } private void getDefaultFieldElements(CodeUnit cu, List elements) { @@ -205,7 +209,7 @@ public class PlateFieldFactory extends FieldFactory { return false; } - private String getCommentText(CodeUnit cu) { + private String getCommentText(CodeUnit cu, List offcutComments) { String[] comments = cu.getCommentAsArray(CodeUnit.PLATE_COMMENT); if (comments == null) { return null; @@ -218,6 +222,12 @@ public class PlateFieldFactory extends FieldFactory { } buffy.append(comment); } + for (String offcut : offcutComments) { + if (buffy.length() != 0) { + buffy.append('\n'); + } + buffy.append(offcut); + } return buffy.toString(); } @@ -226,8 +236,8 @@ public class PlateFieldFactory extends FieldFactory { * data is clipped because it is too long to display. */ private boolean generateFormattedPlateComment(List elements, String[] comments, - Program p) { - if (comments == null || comments.length == 0) { + List offcutComments, Program p) { + if (offcutComments.isEmpty() && CollectionUtils.isBlank(comments)) { return false; } @@ -245,6 +255,11 @@ public class PlateFieldFactory extends FieldFactory { for (String c : comments) { commentsList.add(CommentUtils.parseTextForAnnotations(c, p, prototype, row++)); } + for (String offcut : offcutComments) { + AttributedString as = new AttributedString(offcut, CommentColors.OFFCUT, + getMetrics(style), false, null); + commentsList.add(new TextFieldElement(as, commentsList.size(), 0)); + } if (isWordWrap) { int spaceWidth = getMetrics().charWidth(' '); @@ -554,7 +569,8 @@ public class PlateFieldFactory extends FieldFactory { */ CodeUnit cu = (CodeUnit) obj; - String commentText = getCommentText(cu); + List offcutComments = CommentUtils.getOffcutComments(cu, CommentType.PLATE); + String commentText = getCommentText(cu, offcutComments); boolean hasComment = true; if (StringUtils.isBlank(commentText)) { String defaultComment = getDefaultComment(cu); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PostCommentFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PostCommentFieldFactory.java index f90ffda7bc..a198097ceb 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PostCommentFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PostCommentFieldFactory.java @@ -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. @@ -137,21 +137,22 @@ public class PostCommentFieldFactory extends FieldFactory { } String[] autoComment = getAutoPostComment(cu); + List offcutComments = CommentUtils.getOffcutComments(cu, CommentType.POST); String[] comments = cu.getCommentAsArray(CodeUnit.POST_COMMENT); if (comments != null && comments.length > 0 && (cu instanceof Data)) { - return getTextField(comments, autoComment, proxy, x, false); + return getTextField(comments, autoComment, offcutComments, proxy, x, false); } if (cu instanceof Instruction) { Instruction instr = (Instruction) cu; if (instr.getDelaySlotDepth() > 0) { if (comments != null && comments.length > 0) { - return getTextField(comments, null, proxy, x, false); + return getTextField(comments, null, offcutComments, proxy, x, false); } return null; } // check field options - return getTextFieldForOptions(instr, comments, autoComment, proxy, x); + return getTextFieldForOptions(instr, comments, autoComment, offcutComments, proxy, x); } return null; } @@ -386,7 +387,7 @@ public class PostCommentFieldFactory extends FieldFactory { } private ListingTextField getTextFieldForOptions(Instruction instr, String[] comments, - String[] autoComment, ProxyObj proxy, int xStart) { + String[] autoComment, List offcutComments, ProxyObj proxy, int xStart) { Listing listing = instr.getProgram().getListing(); Address addr = instr.getMinAddress(); FlowType flowType = instr.getFlowType(); @@ -405,14 +406,14 @@ public class PostCommentFieldFactory extends FieldFactory { String[] str = new String[] { FUN_EXIT_FLAG_LEADER + function.getName() + FUN_EXIT_FLAG_TAIL }; - return getTextField(str, autoComment, proxy, xStart, true); + return getTextField(str, autoComment, offcutComments, proxy, xStart, true); } } } // Add Jump/Terminator if (flagJMPsRETs && !instr.hasFallthrough()) { String[] str = new String[] { DEFAULT_FLAG_COMMENT }; - return getTextField(str, autoComment, proxy, xStart, true); + return getTextField(str, autoComment, offcutComments, proxy, xStart, true); } } @@ -472,11 +473,12 @@ public class PostCommentFieldFactory extends FieldFactory { } } } - return getTextField(comments, autoComment, proxy, xStart, false); + return getTextField(comments, autoComment, offcutComments, proxy, xStart, false); } private ListingTextField getTextField(String[] comments, String[] autoComment, - ProxyObj proxy, int xStart, boolean useLinesAfterBlock) { + List offcutComments, ProxyObj proxy, int xStart, + boolean useLinesAfterBlock) { if (comments == null) { comments = EMPTY_STRING_ARRAY; @@ -489,7 +491,8 @@ public class PostCommentFieldFactory extends FieldFactory { ((comments.length == 0 && !useLinesAfterBlock) || alwaysShowAutomatic) ? autoComment.length : 0; - if (!useLinesAfterBlock && comments.length == 0 && nLinesAutoComment == 0) { + if (!useLinesAfterBlock && comments.length == 0 && nLinesAutoComment == 0 && + offcutComments.isEmpty()) { return null; } @@ -507,6 +510,11 @@ public class PostCommentFieldFactory extends FieldFactory { fields.add(CommentUtils.parseTextForAnnotations(comment, program, prototypeString, fields.size())); } + for (String offcutComment : offcutComments) { + AttributedString as = new AttributedString(offcutComment, CommentColors.OFFCUT, + getMetrics(style), false, null); + fields.add(new TextFieldElement(as, fields.size(), 0)); + } if (isWordWrap) { fields = FieldUtils.wrap(fields, width); } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PreCommentFieldFactory.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PreCommentFieldFactory.java index 3f3dca3358..840a3988e3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PreCommentFieldFactory.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/PreCommentFieldFactory.java @@ -112,8 +112,9 @@ public class PreCommentFieldFactory extends FieldFactory { String[] autoComment = getAutoPreComments(cu); String[] comments = getDefinedPreComments(cu); + List offcutComments = CommentUtils.getOffcutComments(cu, CommentType.PRE); - return getTextField(comments, autoComment, proxy, x); + return getTextField(comments, autoComment, offcutComments, proxy, x); } private String[] getDefinedPreComments(CodeUnit cu) { @@ -353,7 +354,7 @@ public class PreCommentFieldFactory extends FieldFactory { } private ListingTextField getTextField(String[] comments, String[] autoComment, - ProxyObj proxy, int xStart) { + List offcutComments, ProxyObj proxy, int xStart) { if (comments == null) { comments = EMPTY_STRING_ARRAY; @@ -364,7 +365,8 @@ public class PreCommentFieldFactory extends FieldFactory { int nLinesAutoComment = (comments.length == 0 || alwaysShowAutomatic) ? autoComment.length : 0; - if (comments.length == 0 && nLinesAutoComment == 0) { + + if (comments.length == 0 && nLinesAutoComment == 0 && offcutComments.isEmpty()) { return null; } @@ -382,6 +384,12 @@ public class PreCommentFieldFactory extends FieldFactory { fields.add(CommentUtils.parseTextForAnnotations(comment, program, prototypeString, fields.size())); } + for (String offcutComment : offcutComments) { + AttributedString as = new AttributedString(offcutComment, CommentColors.OFFCUT, + getMetrics(style), false, null); + fields.add(new TextFieldElement(as, fields.size(), 0)); + } + if (isWordWrap) { fields = FieldUtils.wrap(fields, width); }