diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java index bbad853450..2a39ad9d05 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/Annotation.java @@ -16,8 +16,6 @@ package ghidra.app.util.viewer.field; import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import docking.widgets.fieldpanel.field.AttributedString; import ghidra.app.nav.Navigatable; @@ -26,12 +24,8 @@ import ghidra.program.model.listing.Program; import ghidra.util.classfinder.ClassSearcher; public class Annotation { - /** - * A pattern to match text between two quote characters and to capture that text. This - * pattern does not match quote characters that are escaped with a '\' character. - */ - private static final Pattern QUOTATION_PATTERN = - Pattern.compile("(? ANNOTATED_STRING_HANDLERS; private static Map ANNOTATED_STRING_MAP; @@ -149,49 +143,6 @@ public class Annotation { serviceProvider); } - private String[] parseAnnotationText(String theAnnotationText) { - StringBuilder buffer = new StringBuilder(theAnnotationText); - - // strip off the brackets - buffer.delete(0, 2); // remove '{' and '@' - buffer.deleteCharAt(buffer.length() - 1); - - // first split out the tokens on '"' so that annotations can have groupings with - // whitespace - int unqouotedOffset = 0; - List tokens = new ArrayList<>(); - Matcher matcher = QUOTATION_PATTERN.matcher(buffer.toString()); - while (matcher.find()) { - // put all text in the buffer, - int quoteStart = matcher.start(); - String contentBeforeQuote = buffer.substring(unqouotedOffset, quoteStart); - grabTokens(tokens, contentBeforeQuote); - unqouotedOffset = matcher.end(); - - String quotedContent = matcher.group(1); // group 0 is the entire string - tokens.add(quotedContent); - } - - // handle any remaining part of the text after quoted sections - if (unqouotedOffset < buffer.length()) { - String remainingString = buffer.substring(unqouotedOffset); - grabTokens(tokens, remainingString); - } - - // split on whitespace - return tokens.toArray(new String[tokens.size()]); - } - - private void grabTokens(List tokenContainer, String content) { - String[] strings = content.split("\\s"); - for (String string : strings) { - // 0 length strings can happen when 'content' begins with a space - if (string.length() > 0) { - tokenContainer.add(string); - } - } - } - public String getAnnotationText() { return annotationText; } @@ -204,4 +155,137 @@ public class Annotation { /*package*/ static Set getAnnotationNames() { return Collections.unmodifiableSet(getAnnotatedStringHandlerMap().keySet()); } + + private String[] parseAnnotationText(String text) { + + String trimmed = text.substring(2, text.length() - 1); // remove "{@" and '}' + List tokens = new ArrayList<>(); + List parts = parseText(trimmed); + for (TextPart part : parts) { + part.grabTokens(tokens); + } + + return tokens.toArray(new String[tokens.size()]); + } + + private List parseText(String text) { + + List textParts = new ArrayList<>(); + boolean escaped = false; + boolean inQuote = false; + int partStart = 0; + int n = text.length(); + for (int i = 0; i < n; i++) { + + boolean wasEscaped = escaped; + escaped = false; + char prev = '\0'; + if (i != 0 && !wasEscaped) { + prev = text.charAt(i - 1); + } + + char c = text.charAt(i); + if (prev == '\\') { + if (Annotation.ESCAPABLE_CHARS.indexOf(c) != -1) { + escaped = true; + continue; + } + } + + if (c == '"') { + if (inQuote) { + // end quote + String s = text.substring(partStart, i + 1); // keep the quote + textParts.add(new QuotedTextPart(s)); + partStart = i + 1; + } + else { + // end previous word; start quote + if (i != 0) { + String s = text.substring(partStart, i); + textParts.add(new TextPart(s)); + partStart = i; + } + } + inQuote = !inQuote; + } + } + + if (partStart < n) { // grab trailing text + String s = text.substring(partStart, n); + textParts.add(new TextPart(s)); + } + + return textParts; + } + + // remove any backslashes that escape special annotation characters, like '{' and '}' + private static String removeEscapeChars(String text) { + boolean escaped = false; + StringBuilder buffy = new StringBuilder(); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + boolean wasEscaped = escaped; + escaped = false; + if (c != '\\') { + buffy.append(c); + continue; + } + + char next = '\0'; + if (i != text.length() - 1 && !wasEscaped) { + next = text.charAt(i + 1); + } + + if (ESCAPABLE_CHARS.indexOf(next) != -1) { + escaped = true; + continue; + } + buffy.append(c); + } + + return buffy.toString(); + } + + /** + * A simple class to hold text and extract tokens + */ + private class TextPart { + + protected String text; + + TextPart(String text) { + this.text = text; + } + + public void grabTokens(List tokens) { + String escaped = removeEscapeChars(text); + String[] strings = escaped.split("\\s"); + for (String string : strings) { + // 0 length strings can happen when 'content' begins with a space + if (string.length() > 0) { + tokens.add(string); + } + } + } + + @Override + public String toString() { + return text; + } + } + + private class QuotedTextPart extends TextPart { + QuotedTextPart(String text) { + super(text); + } + + @Override + public void grabTokens(List tokens) { + String unquoted = text.substring(1, text.length() - 1); + String escaped = removeEscapeChars(unquoted); + tokens.add(escaped); // all quoted text is a 'token' + } + } + } 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 85f56eb754..0ee2b46127 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 @@ -191,8 +191,7 @@ public class CommentUtils { List annotations = getCommentAnnotations(text); if (annotations.isEmpty()) { - String updatedText = removeEscapeChars(text); - results.add(new StringCommentPart(text, updatedText, prototype)); + results.add(new StringCommentPart(text, prototype)); return results; } @@ -204,23 +203,20 @@ public class CommentUtils { if (offset != start) { // text between annotations String preceeding = text.substring(offset, start); - String updatedText = removeEscapeChars(preceeding); - results.add(new StringCommentPart(preceeding, updatedText, prototype)); + results.add(new StringCommentPart(preceeding, prototype)); } String annotationText = word.getWord(); - String updatedText = removeEscapeChars(annotationText); - Annotation annotation = new Annotation(updatedText, prototype, program); + Annotation annotation = new Annotation(annotationText, prototype, program); annotation = fixerUpper.apply(annotation); - results.add(new AnnotationCommentPart(updatedText, annotation)); + results.add(new AnnotationCommentPart(annotationText, annotation)); offset = start + annotationText.length(); } if (offset != text.length()) { // trailing text String trailing = text.substring(offset); - String updatedText = removeEscapeChars(trailing); - results.add(new StringCommentPart(trailing, updatedText, prototype)); + results.add(new StringCommentPart(trailing, prototype)); } return results; @@ -269,23 +265,6 @@ public class CommentUtils { //@formatter:on } - // remove any backslashes that escape special annotation characters, like '{' and '}' - private static String removeEscapeChars(String text) { - StringBuilder buffy = new StringBuilder(); - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == '\\') { - char next = i == text.length() ? '\0' : text.charAt(i + 1); - if (next == '{' || next == '}') { - continue; - } - } - buffy.append(c); - } - - return buffy.toString(); - } - /* * Starts at the given index and looks for the end an annotation, ignoring quoted text * and escaped characters along the way. The value returned is the index after the last @@ -294,26 +273,30 @@ public class CommentUtils { */ private static int findAnnotationEnd(String comment, int start) { - boolean startQuote = false; - int count = 0; + boolean escaped = false; + boolean inQuote = false; for (int i = start; i < comment.length(); i++) { - char prev = i == 0 ? '\0' : comment.charAt(i - 1); - if (prev == '\\') { - continue; // escaped + + boolean wasEscaped = escaped; + escaped = false; + char prev = '\0'; + if (i != 0 && !wasEscaped) { + prev = comment.charAt(i - 1); } char c = comment.charAt(i); + if (prev == '\\') { + if (Annotation.ESCAPABLE_CHARS.indexOf(c) != -1) { + escaped = true; + continue; + } + } + if (c == '"') { - if (startQuote) { - --count; - } - else { - ++count; - } - startQuote = !startQuote; + inQuote = !inQuote; } else if (c == '}') { - if (count == 0) { + if (!inQuote) { return i + 1; } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/StringCommentPart.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/StringCommentPart.java index e94a17b7b6..5fbc7d0bc8 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/StringCommentPart.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/field/StringCommentPart.java @@ -20,21 +20,15 @@ import docking.widgets.fieldpanel.field.*; public class StringCommentPart extends CommentPart { private AttributedString prototype; - private String rawText; - StringCommentPart(String rawText, AttributedString prototype) { - this(rawText, rawText, prototype); - } - - StringCommentPart(String rawText, String displayText, AttributedString prototype) { - super(displayText); - this.rawText = rawText; + StringCommentPart(String text, AttributedString prototype) { + super(text); this.prototype = prototype; } @Override String getRawText() { - return rawText; + return getDisplayText(); } @Override @@ -45,6 +39,6 @@ public class StringCommentPart extends CommentPart { @Override public String toString() { - return rawText; + return getDisplayText(); } } 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 bb54810947..e9048fe1f7 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 @@ -206,16 +206,64 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest { @Test public void testSymbolAnnotation_WithEscapedItemsOutsideOfAnnotation() { - String rawComment = "This is a foo\\} symbol {@sym mySym\\{0\\}} annotation \\{bar"; + String rawComment = "This is a foo} symbol {@sym mySym\\{0\\}} annotation {bar"; String display = CommentUtils.getDisplayString(rawComment, program); assertEquals("This is a foo} symbol mySym{0} annotation {bar", display); } + @Test + public void testAddressAnnotation_QuotedQuote() { + String rawComment = "Test {@address 0 \"quote\\\"\"} extra}"; + String display = CommentUtils.getDisplayString(rawComment, program); + assertEquals("Test quote\" extra}", display); + } + + @Test + public void testAddressAnnotation_EscapedBrace() { + String rawComment = "Test {@address 0 \"quote\\}\"} blah"; + String display = CommentUtils.getDisplayString(rawComment, program); + assertEquals("Test quote} blah", display); + } + + @Test + public void testAddressAnnotation_BackslashAndEscapedBrace() { + String rawComment = "Test {@address 0 \"quote\\\\}\"} blah"; + String display = CommentUtils.getDisplayString(rawComment, program); + assertEquals("Test quote\\} blah", display); + } + + @Test + public void testAddressAnnotation_BackslashBackslash() { + String rawComment = "Test {@address 0 \"quote\\\\\"} blah"; + String display = CommentUtils.getDisplayString(rawComment, program); + assertEquals("Test quote\\ blah", display); + } + + @Test + public void testAddressAnnotation_LonelyBackslash() { + String rawComment = "Test {@address 0 bo\\b} some text"; + String display = CommentUtils.getDisplayString(rawComment, program); + assertEquals("Test bo\\b some text", display); + } + @Test public void testSymbolAnnotation_FullyEscaped() { + // We currently don't support rendering escaped annotation characters unless they are + // inside of an annotation. String rawComment = "This is a symbol \\{@sym bob\\} annotation"; String display = CommentUtils.getDisplayString(rawComment, program); - assertEquals("This is a symbol {@sym bob} annotation", display); + assertEquals("This is a symbol \\{@sym bob\\} annotation", display); + } + + @Test + public void testSymbolAnnotation_LonelyBackslash() { + // We currently don't support rendering escaped annotation characters unless they are + // inside of an annotation. + String rawComment = "This is a symbol {@sym bob jo\\e} annotation"; + String display = CommentUtils.getDisplayString(rawComment, program); + + // Note: Symbol Annotations ignore display text which means that the symbol name + assertEquals("This is a symbol bob annotation", display); } @Test diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/CommentUtilsTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/CommentUtilsTest.java index e0bdea773c..e0a6ff19d5 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/CommentUtilsTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/CommentUtilsTest.java @@ -37,7 +37,7 @@ public class CommentUtilsTest extends AbstractGhidraHeadlessIntegrationTest { @Test public void testGetCommentAnnotations_PlainAnnotation() { - String comment = "This is an {@symbol symbolName}"; + String comment = "This is a {@symbol symbolName}"; List annotations = CommentUtils.getCommentAnnotations(comment); assertEquals(1, annotations.size()); WordLocation word = annotations.get(0); @@ -47,7 +47,7 @@ public class CommentUtilsTest extends AbstractGhidraHeadlessIntegrationTest { @Test public void testGetCommentAnnotations_QuotedAnnotation() { - String comment = "This is an {@symbol \"symbolName\"}"; + String comment = "This is a {@symbol \"symbolName\"}"; List annotations = CommentUtils.getCommentAnnotations(comment); assertEquals(1, annotations.size()); WordLocation word = annotations.get(0); @@ -57,7 +57,7 @@ public class CommentUtilsTest extends AbstractGhidraHeadlessIntegrationTest { @Test public void testGetCommentAnnotations_QuotedAnnotation_WithEscapedQuotes() { - String comment = "This is an {@symbol \"symbol\\\"Name\\\"\"}"; + String comment = "This is a {@symbol \"symbol\\\"Name\\\"\"}"; List annotations = CommentUtils.getCommentAnnotations(comment); assertEquals(1, annotations.size()); WordLocation word = annotations.get(0); @@ -67,7 +67,7 @@ public class CommentUtilsTest extends AbstractGhidraHeadlessIntegrationTest { @Test public void testGetCommentAnnotations_QuotedAnnotationWithBraces() { - String comment = "This is an {@symbol \"symbol{Name}\"}"; + String comment = "This is a {@symbol \"symbol{Name}\"}"; List annotations = CommentUtils.getCommentAnnotations(comment); assertEquals(1, annotations.size()); WordLocation word = annotations.get(0); @@ -79,7 +79,7 @@ public class CommentUtilsTest extends AbstractGhidraHeadlessIntegrationTest { // the second brace is ignored (if the first brace is part of the symbol name, then it // needs to be escaped or quoted - String comment = "This is an {@symbol symbol{Name}}"; + String comment = "This is a {@symbol symbol{Name}}"; List annotations = CommentUtils.getCommentAnnotations(comment); assertEquals(1, annotations.size()); WordLocation word = annotations.get(0); @@ -90,7 +90,7 @@ public class CommentUtilsTest extends AbstractGhidraHeadlessIntegrationTest { public void testGetCommentAnnotations_UnquotedAnnotation_WithUnbalancedBraces() { // the second brace is ignored - String comment = "This is an {@symbol symbolName}}"; + String comment = "This is a {@symbol symbolName}}"; List annotations = CommentUtils.getCommentAnnotations(comment); assertEquals(1, annotations.size()); WordLocation word = annotations.get(0); @@ -101,7 +101,7 @@ public class CommentUtilsTest extends AbstractGhidraHeadlessIntegrationTest { public void testGetCommentAnnotations_UnquotedAnnotation_WithEscapedBraces() { // escaped braces get ignored - String comment = "This is an {@symbol symbol\\{Name\\}}"; + String comment = "This is a {@symbol symbol\\{Name\\}}"; List annotations = CommentUtils.getCommentAnnotations(comment); assertEquals(1, annotations.size()); WordLocation word = annotations.get(0); diff --git a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgComponent.java b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgComponent.java index c2cd471364..aabcc6167b 100644 --- a/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgComponent.java +++ b/Ghidra/Features/GraphFunctionCalls/src/main/java/functioncalls/graph/view/FcgComponent.java @@ -53,12 +53,6 @@ public class FcgComponent extends GraphComponent