Merge remote-tracking branch 'origin/GP-3760-dragonmacher-annotations-text--SQUASHED'

This commit is contained in:
Ryan Kurtz 2023-09-20 11:53:14 -04:00
commit 2623a95a0d
9 changed files with 363 additions and 124 deletions

View File

@ -56,7 +56,7 @@ public class AddressAnnotatedStringHandler implements AnnotatedStringHandler {
String addressText = address.toString();
if (text.length > 2) { // address and display text
StringBuffer buffer = new StringBuffer();
StringBuilder buffer = new StringBuilder();
for (int i = 2; i < text.length; i++) {
buffer.append(text[i]).append(" ");
}

View File

@ -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,55 +143,149 @@ public class Annotation {
serviceProvider);
}
private String[] parseAnnotationText(String theAnnotationText) {
StringBuffer buffer = new StringBuffer(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;
}
@Override
public String toString() {
return annotationText;
}
/*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'
}
}
}

View File

@ -0,0 +1,44 @@
/* ###
* 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.AbstractTextFieldElement;
import ghidra.util.bean.field.AnnotatedTextFieldElement;
public class AnnotationCommentPart extends CommentPart {
private Annotation annotation;
AnnotationCommentPart(String displayText, Annotation annotation) {
super(displayText);
this.annotation = annotation;
}
@Override
String getRawText() {
return annotation.getAnnotationText();
}
@Override
AbstractTextFieldElement createElement(int row, int column) {
return new AnnotatedTextFieldElement(annotation, row, column);
}
@Override
public String toString() {
return annotation.toString();
}
}

View File

@ -0,0 +1,35 @@
/* ###
* 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.AbstractTextFieldElement;
public abstract class CommentPart {
protected String displayText;
CommentPart(String displayText) {
this.displayText = displayText;
}
abstract AbstractTextFieldElement createElement(int row, int column);
abstract String getRawText();
String getDisplayText() {
return displayText;
}
}

View File

@ -31,8 +31,6 @@ import generic.theme.Gui;
import ghidra.program.model.listing.Program;
import ghidra.util.StringUtilities;
import ghidra.util.WordLocation;
import ghidra.util.bean.field.AnnotatedTextFieldElement;
import ghidra.util.exception.AssertException;
public class CommentUtils {
@ -75,21 +73,10 @@ public class CommentUtils {
};
StringBuilder buffy = new StringBuilder();
List<Object> parts =
List<CommentPart> parts =
doParseTextIntoTextAndAnnotations(rawCommentText, symbolFixer, program, prototype);
for (Object part : parts) {
if (part instanceof String) {
String s = (String) part;
buffy.append(s);
}
else if (part instanceof Annotation) {
Annotation a = (Annotation) part;
buffy.append(a.getAnnotationText());
}
else {
throw new AssertException("Unhandled annotation piece: " + part);
}
for (CommentPart part : parts) {
buffy.append(part.getRawText());
}
return buffy.toString();
}
@ -136,7 +123,7 @@ public class CommentUtils {
Function<Annotation, Annotation> noFixing = Function.identity();
return doParseTextForAnnotations(text, noFixing, program, prototypeString, row);
}
/**
* Sanitizes the given text, removing or replacing illegal characters.
* <p>
@ -175,25 +162,12 @@ public class CommentUtils {
text = StringUtilities.convertTabsToSpaces(text);
int column = 0;
List<Object> parts =
List<CommentPart> parts =
doParseTextIntoTextAndAnnotations(text, fixerUpper, program, prototype);
List<FieldElement> fields = new ArrayList<>();
for (Object part : parts) {
if (part instanceof String) {
String s = (String) part;
AttributedString as = prototype.deriveAttributedString(s);
fields.add(new TextFieldElement(as, row, column));
column += s.length();
}
else if (part instanceof Annotation) {
Annotation a = (Annotation) part;
fields.add(new AnnotatedTextFieldElement(a, row, column));
column += a.getAnnotationText().length();
}
else {
throw new AssertException("Unhandled annotation piece: " + part);
}
for (CommentPart part : parts) {
fields.add(part.createElement(row, column));
column += part.getDisplayText().length();
}
return new CompositeFieldElement(fields.toArray(new FieldElement[fields.size()]));
@ -204,21 +178,20 @@ public class CommentUtils {
* an Annotation
*
* @param text the text to parse
* @param fixerUpper a function that is given a chance to convert an Annotation into a new
* one
* @param fixerUpper a function that is given a chance to convert an Annotation into a new one
* @param program the program
* @param prototype the prototype string that contains decoration attributes
* @return a list that contains a mixture String or an Annotation entries
*/
private static List<Object> doParseTextIntoTextAndAnnotations(String text,
private static List<CommentPart> doParseTextIntoTextAndAnnotations(String text,
Function<Annotation, Annotation> fixerUpper, Program program,
AttributedString prototype) {
List<Object> results = new ArrayList<>();
List<CommentPart> results = new ArrayList<>();
List<WordLocation> annotations = getCommentAnnotations(text);
if (annotations.isEmpty()) {
results.add(text);
results.add(new StringCommentPart(text, prototype));
return results;
}
@ -230,19 +203,20 @@ public class CommentUtils {
if (offset != start) {
// text between annotations
String preceeding = text.substring(offset, start);
results.add(preceeding);
results.add(new StringCommentPart(preceeding, prototype));
}
String annotationText = word.getWord();
Annotation annotation = new Annotation(annotationText, prototype, program);
annotation = fixerUpper.apply(annotation);
results.add(annotation);
results.add(new AnnotationCommentPart(annotationText, annotation));
offset = start + annotationText.length();
}
if (offset != text.length()) { // trailing text
results.add(text.substring(offset));
String trailing = text.substring(offset);
results.add(new StringCommentPart(trailing, prototype));
}
return results;
@ -299,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;
}
}

View File

@ -0,0 +1,44 @@
/* ###
* 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.*;
public class StringCommentPart extends CommentPart {
private AttributedString prototype;
StringCommentPart(String text, AttributedString prototype) {
super(text);
this.prototype = prototype;
}
@Override
String getRawText() {
return getDisplayText();
}
@Override
AbstractTextFieldElement createElement(int row, int column) {
AttributedString as = prototype.deriveAttributedString(displayText);
return new TextFieldElement(as, row, column);
}
@Override
public String toString() {
return getDisplayText();
}
}

View File

@ -201,14 +201,69 @@ public class AnnotationTest extends AbstractGhidraHeadedIntegrationTest {
public void testSymbolAnnotation_WithBracesInName_Escaped() {
String rawComment = "This is a symbol {@sym mySym\\{0\\}} annotation";
String display = CommentUtils.getDisplayString(rawComment, program);
assertEquals("This is a symbol mySym\\{0\\} annotation", display);
assertEquals("This is a symbol mySym{0} annotation", display);
}
@Test
public void testSymbolAnnotation_WithEscapedItemsOutsideOfAnnotation() {
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(rawComment, 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

View File

@ -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,16 +101,17 @@ 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);
assertEquals("{@symbol symbol\\{Name\\}}", word.getWord());
}
@Test
public void testSanitize() {
String comment = null;
String sanitized = CommentUtils.sanitize(comment);
assertNull(sanitized);

View File

@ -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();