mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-05-20 01:53:42 +08:00
Updated annotations to support escaping some characters inside of the annotation text
This commit is contained in:
@@ -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("(?<!\\\\)[\"](.*?)(?<!\\\\)[\"]");
|
||||
|
||||
public static final String ESCAPABLE_CHARS = "{}\"\\";
|
||||
|
||||
private static List<AnnotatedStringHandler> ANNOTATED_STRING_HANDLERS;
|
||||
private static Map<String, AnnotatedStringHandler> 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<String> 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<String> 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<String> getAnnotationNames() {
|
||||
return Collections.unmodifiableSet(getAnnotatedStringHandlerMap().keySet());
|
||||
}
|
||||
|
||||
private String[] parseAnnotationText(String text) {
|
||||
|
||||
String trimmed = text.substring(2, text.length() - 1); // remove "{@" and '}'
|
||||
List<String> tokens = new ArrayList<>();
|
||||
List<TextPart> parts = parseText(trimmed);
|
||||
for (TextPart part : parts) {
|
||||
part.grabTokens(tokens);
|
||||
}
|
||||
|
||||
return tokens.toArray(new String[tokens.size()]);
|
||||
}
|
||||
|
||||
private List<TextPart> parseText(String text) {
|
||||
|
||||
List<TextPart> 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<String> 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<String> tokens) {
|
||||
String unquoted = text.substring(1, text.length() - 1);
|
||||
String escaped = removeEscapeChars(unquoted);
|
||||
tokens.add(escaped); // all quoted text is a 'token'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -191,8 +191,7 @@ public class CommentUtils {
|
||||
|
||||
List<WordLocation> 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;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-10
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+50
-2
@@ -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
|
||||
|
||||
+7
-7
@@ -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<WordLocation> 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<WordLocation> 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<WordLocation> 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<WordLocation> 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<WordLocation> 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<WordLocation> 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<WordLocation> annotations = CommentUtils.getCommentAnnotations(comment);
|
||||
assertEquals(1, annotations.size());
|
||||
WordLocation word = annotations.get(0);
|
||||
|
||||
-6
@@ -53,12 +53,6 @@ public class FcgComponent extends GraphComponent<FcgVertex, FcgEdge, FunctionCal
|
||||
build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setGraph(FunctionCallGraph g) {
|
||||
this.fcGraph = g;
|
||||
super.setGraph(g);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FcgVertex getInitialVertex() {
|
||||
return fcGraph.getSource();
|
||||
|
||||
Reference in New Issue
Block a user