review fixes; waiting for master update

GP-4839 - Decompiler Text Finder - Added the ability to search decompiled text
This commit is contained in:
dragonmacher 2024-10-28 17:39:11 -04:00
parent 9e589a451a
commit 90eba4f9e0
32 changed files with 1815 additions and 524 deletions

View File

@ -290,7 +290,7 @@
only the entry point bookmarks, you would enter "entry" in the filter field. The results
would show only those bookmarks with a Category or Description containing the word "entry".
The text filter is not case sensitive, nor does it support <I><A href=
"help/topics/Glossary/glossary.htm#RegularExpression">regular expressions</A></I>.</P>
"help/topics/Glossary/glossary.htm#RegularExpression">Regular Expressions</A></I>.</P>
</BLOCKQUOTE>
<H3><B>Reorder Columns</B></H3>

View File

@ -1111,7 +1111,10 @@ xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-
<H2><A name="RegularExpression"></A>Regular Expression</H2>
<BLOCKQUOTE>
<P>A character sequence used to match patterns in strings.</P>
<P>A character sequence used to match patterns in strings. See
<A href="help/topics/Search/Regular_Expressions.htm#Regex_Syntax">Regular Expression</A>
for examples.
</P>
</BLOCKQUOTE>
<H2><A name="RelocationTable"></A>Relocation Table</H2>

View File

@ -124,7 +124,7 @@ public class ProgramLocationActionContext extends ProgramActionContext
return functions;
}
private Function getFunctionForLocation() {
protected Function getFunctionForLocation() {
if (location instanceof FunctionLocation functionLocation) {
Address functionAddress = functionLocation.getFunctionAddress();
return program.getFunctionManager().getFunctionAt(functionAddress);

View File

@ -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.
@ -25,6 +25,7 @@ import docking.action.DockingAction;
import docking.action.MenuData;
import generic.theme.GIcon;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.FunctionSupplierContext;
import ghidra.app.context.ListingActionContext;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
@ -145,7 +146,15 @@ public class CallTreePlugin extends ProgramPlugin {
@Override
public boolean isAddToPopup(ActionContext context) {
return (context instanceof ListingActionContext);
if (context instanceof ListingActionContext) {
return true;
}
if (context instanceof FunctionSupplierContext functionContext) {
return functionContext.hasFunctions();
}
return false;
}
};

View File

@ -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.
@ -92,6 +92,18 @@ public class LocationReferenceContext {
return buffy.toString();
}
/**
* Returns text that is helpful for debugging, such as printing to a console.
* @return the text
*/
public String getDebugText() {
StringBuilder buffy = new StringBuilder();
for (Part part : parts) {
buffy.append(part.getDebugText());
}
return buffy.toString();
}
/**
* Returns HTML text for this context. Any matching items embedded in the returned string will
* be bold.
@ -142,7 +154,7 @@ public class LocationReferenceContext {
abstract String getHtmlText();
abstract String getText(String start, String end);
abstract String getDebugText();
static String fixBreakingSpaces(String s) {
String updated = s.replaceAll("\\s", "&nbsp;");
@ -165,8 +177,8 @@ public class LocationReferenceContext {
}
@Override
String getText(String start, String end) {
return text; // we don't decorate
String getDebugText() {
return text;
}
@Override
@ -187,8 +199,8 @@ public class LocationReferenceContext {
}
@Override
String getText(String start, String end) {
return start + text + end;
String getDebugText() {
return " [[ " + text + " ]] ";
}
@Override

View File

@ -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.
@ -58,6 +58,19 @@ public class LocationReferenceContextBuilder {
return this;
}
/**
* Adds a newline character to the previously added text.
* @return this builder
*/
public LocationReferenceContextBuilder newline() {
if (parts.isEmpty()) {
throw new IllegalStateException("Cannot add a newline without first appending text");
}
Part last = parts.get(parts.size() - 1);
last.text += '\n';
return this;
}
/**
* Builds a {@link LocationReferenceContext} using the text supplied via the {@code append}
* methods.
@ -67,6 +80,14 @@ public class LocationReferenceContextBuilder {
return new LocationReferenceContext(parts);
}
/**
* Returns true if no text has been added to this builder.
* @return true if no text has been added to this builder
*/
public boolean isEmpty() {
return parts.isEmpty();
}
@Override
public String toString() {
return Json.toString(this);

View File

@ -150,8 +150,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
if (result == null) {
searchDialog.setStatusText("Not found");
}
else if (result.programLocation()
.equals(currentLocation)) {
else if (result.programLocation().equals(currentLocation)) {
searchNext(searchTask.getProgram(), searchNavigatable, textSearcher);
}
else {
@ -376,8 +375,9 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
* Create the action for to pop up the search dialog.
*/
private void createActions() {
String subGroup = getClass().getName();
String subGroup = "d"; // Memory Search uses groups 'a', 'b', and 'c'
//@formatter:off
new ActionBuilder("Search Text", getName())
.menuPath("&Search", "Program &Text...")
.menuGroup("search", subGroup)
@ -406,6 +406,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
searchDialog.repeatSearch();
})
.buildAndInstall(tool);
//@formatter:on
}
protected void updateNavigatable(ActionContext context) {
@ -467,8 +468,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
String textSelection = navigatable.getTextSelection();
ProgramLocation location = navigatable.getLocation();
Address address = location.getAddress();
Listing listing = context.getProgram()
.getListing();
Listing listing = context.getProgram().getListing();
CodeUnit codeUnit = listing.getCodeUnitAt(address);
boolean isInstruction = false;
if (textSelection != null) {
@ -582,8 +582,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
// must have completed too fast for the provider to be set; try something cute
Component focusOwner =
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.getFocusOwner();
KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
return focusOwner; // assume this IS the provider
}
@ -628,8 +627,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
@Override
public Highlight[] createHighlights(String text, ListingField field, int cursorTextOffset) {
Class<? extends FieldFactory> fieldFactoryClass = field.getFieldFactory()
.getClass();
Class<? extends FieldFactory> fieldFactoryClass = field.getFieldFactory().getClass();
if (!doHighlight) {
return NO_HIGHLIGHTS;
@ -652,8 +650,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
return getAllHighlights(text, cursorTextOffset);
}
Address address = searchResult.programLocation()
.getAddress();
Address address = searchResult.programLocation().getAddress();
ProxyObj<?> proxy = field.getProxy();
if (proxy.contains(address)) {
return getSingleSearchHighlight(text, field, cursorTextOffset);
@ -745,8 +742,7 @@ public class SearchTextPlugin extends ProgramPlugin implements OptionsChangeList
return true;
}
Class<? extends FieldFactory> factoryClass = field.getFieldFactory()
.getClass();
Class<? extends FieldFactory> factoryClass = field.getFieldFactory().getClass();
if (searchOptions.searchComments()) {
if (factoryClass == PreCommentFieldFactory.class ||
factoryClass == PlateFieldFactory.class ||

View File

@ -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.
@ -384,6 +384,10 @@ public class TableComponentProvider<T> extends ComponentProviderAdapter
return model;
}
public GhidraTable getTable() {
return threadedPanel.getTable();
}
private void updateTitle() {
setSubTitle(generateSubTitle());
}

View File

@ -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.
@ -25,14 +25,12 @@ import ghidra.program.model.symbol.NameTransformer;
import ghidra.util.StringUtilities;
/**
* This class is used to convert a C/C++ language
* token group into readable C/C++ code.
* This class is used to convert a C/C++ language token group into readable C/C++ code.
*/
public class PrettyPrinter {
/**
* The indent string to use when printing.
*/
private final static NameTransformer IDENTITY = new IdentityNameTransformer();
public final static String INDENT_STRING = " ";
private Function function;
@ -51,7 +49,7 @@ public class PrettyPrinter {
public PrettyPrinter(Function function, ClangTokenGroup tokgroup, NameTransformer transformer) {
this.function = function;
this.tokgroup = tokgroup;
this.transformer = (transformer != null) ? transformer : new IdentityNameTransformer();
this.transformer = transformer != null ? transformer : IDENTITY;
flattenLines();
padEmptyLines();
}
@ -72,8 +70,7 @@ public class PrettyPrinter {
}
/**
* Returns a list of the C language lines contained in the
* C language token group.
* Returns a list of the C language lines contained in the C language token group.
* @return a list of the C language lines
*/
public List<ClangLine> getLines() {
@ -81,37 +78,51 @@ public class PrettyPrinter {
}
/**
* Prints the C language token group
* into a string of C code.
* Prints the C language token group into a string of C code.
* @return a string of readable C code
*/
public DecompiledFunction print() {
StringBuilder buff = new StringBuilder();
for (ClangLine line : lines) {
buff.append(line.getIndentString());
List<ClangToken> tokens = line.getAllTokens();
for (ClangToken token : tokens) {
boolean isToken2Clean = token instanceof ClangFuncNameToken ||
token instanceof ClangVariableToken || token instanceof ClangTypeToken ||
token instanceof ClangFieldToken || token instanceof ClangLabelToken;
//do not clean constant variable tokens
if (isToken2Clean && token.getSyntaxType() == ClangToken.CONST_COLOR) {
isToken2Clean = false;
}
String tokenText = token.getText();
if (isToken2Clean) {
tokenText = transformer.simplify(tokenText);
}
buff.append(tokenText);
}
getText(buff, line, transformer);
buff.append(StringUtilities.LINE_SEPARATOR);
}
return new DecompiledFunction(findSignature(), buff.toString());
}
private static void getText(StringBuilder buff, ClangLine line, NameTransformer transformer) {
buff.append(line.getIndentString());
List<ClangToken> tokens = line.getAllTokens();
for (ClangToken token : tokens) {
boolean isToken2Clean = token instanceof ClangFuncNameToken ||
token instanceof ClangVariableToken || token instanceof ClangTypeToken ||
token instanceof ClangFieldToken || token instanceof ClangLabelToken;
//do not clean constant variable tokens
if (isToken2Clean && token.getSyntaxType() == ClangToken.CONST_COLOR) {
isToken2Clean = false;
}
String tokenText = token.getText();
if (isToken2Clean) {
tokenText = transformer.simplify(tokenText);
}
buff.append(tokenText);
}
}
/**
* Returns the text of the given line as seen in the UI.
* @param line the line
* @return the text
*/
public static String getText(ClangLine line) {
StringBuilder buff = new StringBuilder();
getText(buff, line, IDENTITY);
return buff.toString();
}
private String findSignature() {
int nChildren = tokgroup.numChildren();
for (int i = 0; i < nChildren; ++i) {

View File

@ -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.
@ -23,6 +23,7 @@ import ghidra.app.context.NavigatableActionContext;
import ghidra.app.context.RestrictedAddressSetContext;
import ghidra.app.decompiler.*;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.app.decompiler.component.DecompilerUtils;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Function;
@ -123,8 +124,8 @@ public class DecompilerActionContext extends NavigatableActionContext
if (lineNumber != 0) {
return lineNumber;
}
getTokenAtCursor();
return tokenAtCursor == null ? 0 : tokenAtCursor.getLineParent().getLineNumber();
ClangToken token = getTokenAtCursor();
return token == null ? 0 : token.getLineParent().getLineNumber();
}
public DecompilerPanel getDecompilerPanel() {
@ -152,6 +153,22 @@ public class DecompilerActionContext extends NavigatableActionContext
getComponentProvider().getController().setStatusMessage(msg);
}
// allows this Decompiler action context to signal the location is on a function
@Override
protected Function getFunctionForLocation() {
ClangToken token = getTokenAtCursor();
if (token == null) {
return null;
}
if (token instanceof ClangFuncNameToken functionToken) {
Function function = DecompilerUtils.getFunction(program, functionToken);
return function;
}
return null;
}
/**
* The companion method of {@link #checkActionEnablement(Supplier)}.
*

View File

@ -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.
@ -1062,7 +1062,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
//
// Search
//
String searchGroup = "comment2 - Search Group";
String searchGroup = "Comment2 - Search Group";
subGroupPosition = 0; // reset for the next group
FindAction findAction = new FindAction();

View File

@ -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.
@ -15,6 +15,7 @@
*/
apply from: "$rootProject.projectDir/gradle/distributableGhidraModule.gradle"
apply from: "$rootProject.projectDir/gradle/javaProject.gradle"
apply from: "$rootProject.projectDir/gradle/helpProject.gradle"
apply from: "$rootProject.projectDir/gradle/jacocoProject.gradle"
apply from: "$rootProject.projectDir/gradle/javaTestProject.gradle"
apply plugin: 'eclipse'

View File

@ -1,3 +1,8 @@
##VERSION: 2.0
Module.manifest||GHIDRA||||END|
data/ExtensionPoint.manifest||GHIDRA||||END|
data/decompiler.dependent.theme.properties||GHIDRA||||END|
src/main/help/help/TOC_Source.xml||GHIDRA||||END|
src/main/help/help/topics/DecompilerTextFinderPlugin/Decompiler_Text_Finder.html||GHIDRA||||END|
src/main/help/help/topics/DecompilerTextFinderPlugin/images/DecompilerTextFinderDialog.png||GHIDRA||||END|
src/main/help/help/topics/DecompilerTextFinderPlugin/images/DecompilerTextFinderResultsTable.png||GHIDRA||||END|

View File

@ -0,0 +1,7 @@
[Defaults]
icon.plugin.decompiler.text.finder.select.functions = icon.make.selection {FunctionScope.gif[size(12,12)][move(6,6)]}
[Dark Defaults]

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
This is an XML file intended to be parsed by the Ghidra help system. It is loosely based
upon the JavaHelp table of contents document format. The Ghidra help system uses a
TOC_Source.xml file to allow a module with help to define how its contents appear in the
Ghidra help viewer's table of contents. The main document (in the Base module)
defines a basic structure for the
Ghidra table of contents system. Other TOC_Source.xml files may use this structure to insert
their files directly into this structure (and optionally define a substructure).
In this document, a tag can be either a <tocdef> or a <tocref>. The former is a definition
of an XML item that may have a link and may contain other <tocdef> and <tocref> children.
<tocdef> items may be referred to in other documents by using a <tocref> tag with the
appropriate id attribute value. Using these two tags allows any module to define a place
in the table of contents system (<tocdef>), which also provides a place for
other TOC_Source.xml files to insert content (<tocref>).
During the help build time, all TOC_Source.xml files will be parsed and validated to ensure
that all <tocref> tags point to valid <tocdef> tags. From these files will be generated
<module name>_TOC.xml files, which are table of contents files written in the format
desired by the JavaHelp system. Additionally, the genated files will be merged together
as they are loaded by the JavaHelp system. In the end, when displaying help in the Ghidra
help GUI, there will be on table of contents that has been created from the definitions in
all of the modules' TOC_Source.xml files.
Tags and Attributes
<tocdef>
-id - the name of the definition (this must be unique across all TOC_Source.xml files)
-text - the display text of the node, as seen in the help GUI
-target** - the file to display when the node is clicked in the GUI
-sortgroup - this is a string that defines where a given node should appear under a given
parent. The string values will be sorted by the JavaHelp system using
a javax.text.RulesBasedCollator. If this attribute is not specified, then
the text of attribute will be used.
<tocref>
-id - The id of the <tocdef> that this reference points to
**The URL for the target is relative and should start with 'help/topics'. This text is
used by the Ghidra help system to provide a universal starting point for all links so that
they can be resolved at runtime, across modules.
-->
<tocroot>
<tocref id="Program Search">
<tocdef id="Decompiled Text"
text="Decompiled Text"
target="help/topics/DecompilerTextFinderPlugin/Decompiler_Text_Finder.html" />
</tocref>
</tocroot>

View File

@ -0,0 +1,197 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<TITLE>Search Decompiled Text</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><A name="Search_Decompiled_Text"></A>Search Decompiled Text</H1>
<BLOCKQUOTE>
<P>
Available from <B>Search <IMG SRC="help/shared/arrow.gif" alt="-&gt;" border="0" /> Search
Decompiled Text</B>, this action allows you to search the decompiled output
for each function in the current program.
</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P>
The <B>Decompiled Function Search</B> dialog is shown when users execute the <B>Search
Decompiled Text</B> action. If <I>String Search</I> is selected, then a case insensitive text
search is performed using the specified text. If <I>Regular Expression</I> is selected, then
a <A href="help/topics/Search/Regular_Expressions.htm#Regex_Syntax">Regular Expression</A>
search is performed. Selecting <I>Search Selection</I> will only search function entry points
that are in the current program selection.
</P>
<BR><BR>
<TABLE width="100%">
<TBODY>
<TR>
<TD align="center" width="100%">
<IMG border="1" src="images/DecompilerTextFinderDialog.png"></TD>
</TR>
</TBODY>
<CAPTION align="bottom">
<I>The Decompiled Function Search input dialog</I>
</CAPTION>
</TABLE>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="Note" border="0">When restricting the search to
the current program selection, only functions whose entry point is in the selection will
be searched. The selection will not search functions that have a program selection in
the body, but not at the entry point.
</P>
</BLOCKQUOTE><BR><BR>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="Note" border="0">By default text searches apply
only to one line at a time. If you would like a multi-line search, then you will need
to use a Regular Expression search to do so. See the table below for an example.
</P>
</BLOCKQUOTE><BR><BR>
</BLOCKQUOTE>
<H2>Search Results</H2>
<BLOCKQUOTE>
<P>
The search results will be presented as they are found, as seen in the table below. For single
line search matches, the matching part of the search will be highlighted in the <B>Context</B>
column.
</P>
<BR><BR>
<TABLE width="100%">
<TBODY>
<TR>
<TD align="center" width="100%">
<IMG border="1" src="images/DecompilerTextFinderResultsTable.png" /></TD>
</TR>
</TBODY>
<CAPTION align="bottom">
<I>The Decompiled Function Search results table</I>
</CAPTION>
</TABLE>
<H3>Select Functions Action <IMG BORDER="1" SRC="icon.plugin.decompiler.text.finder.select.functions" />
</H3><A NAME="Select_Functions"></A>
<BLOCKQUOTE>
<P>
This action will create a program selection for each function entry point for each function
selected in the table.
</P>
</BLOCKQUOTE>
<H3>Other Actions</H3>
<BLOCKQUOTE>
<P>
Help for the other table actions can be found
<A HREF="help/topics/Search/Query_Results_Dialog.htm#Make_Selection">here</A>.
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2>Example Searches</H2>
<BLOCKQUOTE>
<P>
The table below show some example searches and lines that they will match. Note that the
reported matches will show the entire line that matched, not just the matching portion of the
line.
</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<CENTER>
<TABLE border="1">
<TR>
<TH align="left" bgcolor="#c0c0c0" valign="top"><B>Search String</B></TH>
<TH align="left" bgcolor="#c0c0c0" valign="top"><B>Description</B></TH>
<TH align="left" bgcolor="#c0c0c0" valign="top"><B>Example Matching Lines</B></TH>
</TR>
<TR>
<TD valign="top">
<CODE>
= '\0'
</CODE>
</TD>
<TD valign="top">
A non-regular expression to find the null character assignment.
</TD>
<TD valign="top" nowrap>
<CODE>
ptr->data[1] = '\0';
</CODE>
</TD>
</TR>
<TR>
<TD valign="top">
<CODE>
set_string\(.*->.*\)
</CODE>
</TD>
<TD valign="top">
A regular expression to find <CODE>set_string(</CODE> followed by any number of characters,
followed by <CODE>-></CODE>, followed by any number of characters and a closing parenthesis.
</TD>
<TD valign="top" nowrap>
<CODE>
set_string(mytable->entry + mytable->numcodes,ptr);
</CODE>
</TD>
</TR>
<TR>
<TD valign="top">
<CODE>
(?s)ffff.*piVar2 =
</CODE>
</TD>
<TD valign="top">
A regular expression that searches across multiple lines by using <CODE>(?S)</CODE>. This
will find <CODE>ffff</CODE> followed by any number of characters, followed by
<CODE>piVar2 =</CODE>.
</TD>
<TD valign="top" nowrap>
<CODE>
if (__CTOR_LIST__ != (code *)0xffffffff) {<BR>
piVar2 = (int *)&__CTOR_LIST__;,
</CODE>
</TD>
</TR>
</TABLE>
</CENTER>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided by: <I>Decompiler Text Finder Plugin</I></P>
<P class="relatedtopic">Related Topics:</P>
<UL>
<LI><A href="help/topics/DecompilePlugin/DecompilerIntro.html">Decompiler</A></LI>
<LI><A href="help/topics/Search/Search_Program_Text.htm">Search Program Text</A></LI>
</UL><BR>
<BR>
</BODY>
</HTML>

View File

@ -0,0 +1,389 @@
/* ###
* 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.plugin.core.search;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import generic.json.Json;
import ghidra.app.decompiler.*;
import ghidra.app.decompiler.component.DecompilerUtils;
import ghidra.app.decompiler.parallel.*;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContextBuilder;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.listing.*;
import ghidra.util.Msg;
import ghidra.util.task.TaskMonitor;
import utility.function.Dummy;
/**
* Searches for the given pattern in all functions in the given program.
*/
public class DecompilerTextFinder {
/**
* Finds text inside decompiled functions using the given pattern.
* @param program the program
* @param searchPattern the search pattern
* @param consumer the consumer that will get matches
* @param monitor the task monitor
* @see #findText(Program, Pattern, Collection, Consumer, TaskMonitor)
*/
public void findText(Program program, Pattern searchPattern, Consumer<TextMatch> consumer,
TaskMonitor monitor) {
monitor = TaskMonitor.dummyIfNull(monitor);
StringFinderCallback callback = new StringFinderCallback(program, searchPattern, consumer);
Listing listing = program.getListing();
FunctionIterator functions = listing.getFunctions(true);
doFindText(program, functions, callback, monitor);
}
/**
* Finds text inside the given decompiled functions using the given pattern.
* @param program the program
* @param searchPattern the search pattern
* @param functions the functions to search
* @param consumer the consumer that will get matches
* @param monitor the task monitor
* @see #findText(Program, Pattern, Consumer, TaskMonitor)
*/
public void findText(Program program, Pattern searchPattern, Iterator<Function> functions,
Consumer<TextMatch> consumer, TaskMonitor monitor) {
monitor = TaskMonitor.dummyIfNull(monitor);
StringFinderCallback callback = new StringFinderCallback(program, searchPattern, consumer);
doFindText(program, functions, callback, monitor);
}
/**
* Finds text inside the given decompiled functions using the given pattern.
* @param program the program
* @param searchPattern the search pattern
* @param functions the functions to search
* @param consumer the consumer that will get matches
* @param monitor the task monitor
* @see #findText(Program, Pattern, Consumer, TaskMonitor)
*/
public void findText(Program program, Pattern searchPattern, Collection<Function> functions,
Consumer<TextMatch> consumer, TaskMonitor monitor) {
monitor = TaskMonitor.dummyIfNull(monitor);
StringFinderCallback callback = new StringFinderCallback(program, searchPattern, consumer);
doFindText(functions, callback, monitor);
}
private void doFindText(Collection<Function> functions, StringFinderCallback callback,
TaskMonitor monitor) {
try {
ParallelDecompiler.decompileFunctions(callback, functions, monitor);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // reset the flag
if (!monitor.isCancelled()) {
Msg.debug(this, "Interrupted while decompiling functions");
}
}
catch (Exception e) {
Msg.error(this, "Encountered an exception decompiling functions", e);
}
finally {
callback.dispose();
}
}
private void doFindText(Program program, Iterator<Function> functions,
StringFinderCallback callback, TaskMonitor monitor) {
Consumer<Void> dummy = Dummy.consumer();
try {
ParallelDecompiler.decompileFunctions(callback, program, functions, dummy, monitor);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // reset the flag
if (!monitor.isCancelled()) {
Msg.debug(this, "Interrupted while decompiling functions");
}
}
catch (Exception e) {
Msg.error(this, "Encountered an exception decompiling functions", e);
}
finally {
callback.dispose();
}
}
private static class StringFinderCallback extends DecompilerCallback<Void> {
private Consumer<TextMatch> callback;
private Pattern pattern;
private String searchText;
StringFinderCallback(Program program, Pattern pattern, Consumer<TextMatch> callback) {
super(program, new DecompilerConfigurer());
this.pattern = pattern;
this.callback = callback;
this.searchText = pattern.pattern();
}
@Override
public Void process(DecompileResults results, TaskMonitor monitor) throws Exception {
Function function = results.getFunction();
if (function.isThunk()) {
return null;
}
ClangTokenGroup tokens = results.getCCodeMarkup();
List<ClangLine> lines = DecompilerUtils.toLines(tokens);
// (?s) - enable dot all mode
// (?-s) - disable dot all mode
boolean multiLine = (pattern.flags() & Pattern.DOTALL) == Pattern.DOTALL;
if (multiLine) {
performMultiLineSearch(function, lines);
}
else {
// line-by-line search
for (ClangLine cLine : lines) {
findMatch(function, cLine);
}
}
return null;
}
private void performMultiLineSearch(Function function, List<ClangLine> lines) {
// Map characters to lines so we can later translate a character position into the line
// that contains it. Also convert all lines into one big run of text to use with the
// regex. Also, turn the c line into a String for later searching.
StringBuilder buffy = new StringBuilder();
TreeMap<Integer, TextLine> linesRangeMap = new TreeMap<>();
int pos = 0;
for (ClangLine cLine : lines) {
String text = PrettyPrinter.getText(cLine);
buffy.append(text).append('\n');
TextLine textLine = new TextLine(pos, cLine, text);
linesRangeMap.put(pos, textLine);
pos += text.length() + 1; // +1 for newline
}
Matcher matcher = pattern.matcher(buffy);
findMatches(function, linesRangeMap, matcher);
}
/**
* Uses the given matcher to search for all matches, which may span multiple lines.
*/
private void findMatches(Function function, TreeMap<Integer, TextLine> linesRangeMap,
Matcher matcher) {
while (matcher.find()) {
emitTextMatch(function, linesRangeMap, matcher);
}
}
private void emitTextMatch(Function function, TreeMap<Integer, TextLine> linesRangeMap,
Matcher matcher) {
List<TextLine> lineMatches = new ArrayList<>();
int pos = 0;
int start = matcher.start();
int end = matcher.end();
for (pos = start; pos < end; pos++) {
// grab the line that contains the current character position
TextLine line = linesRangeMap.floorEntry(pos).getValue();
// This will be positive if the current line contains the match start character.
// In this case, let the line know it has the start. If we don't set the start,
// then the line match will start at 0.
int lineStartOffset = start - line.getOffset(); // relative offset
if (lineStartOffset >= 0) {
line.setMatchStart(lineStartOffset);
}
// In this case, let the line know it has the end. If we don't set the end,
// then the line match will end at the end of the line.
if (end <= line.getEndOffset()) {
int relativeEnd = end - line.getOffset();
line.setMatchEnd(relativeEnd);
}
lineMatches.add(line);
pos = line.getEndOffset();
}
// Use the first line for attributes of the match, like line number and address
TextLine firstLine = lineMatches.get(0);
int lineNumber = firstLine.getLineNumber();
AddressSet addresses = getAddresses(function, firstLine.getCLine());
LocationReferenceContext context = createMatchContext(lineMatches);
TextMatch match =
new TextMatch(function, addresses, lineNumber, searchText, context, true);
callback.accept(match);
}
private LocationReferenceContext createMatchContext(List<TextLine> matches) {
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
for (TextLine line : matches) {
if (!builder.isEmpty()) {
builder.newline();
}
String text = line.getText();
int start = line.getMatchStart();
int end = line.getMatchEnd();
builder.append(text.substring(0, start));
builder.appendMatch(text.substring(start, end));
builder.append(text.substring(end, line.length()));
}
return builder.build();
}
private void findMatch(Function function, ClangLine line) {
String textLine = PrettyPrinter.getText(line);
Matcher matcher = pattern.matcher(textLine);
if (!matcher.find()) {
return;
}
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
int start = matcher.start();
int end = matcher.end();
builder.append(textLine.substring(0, start));
builder.appendMatch(textLine.substring(start, end));
if (end < textLine.length()) {
builder.append(textLine.substring(end));
}
int lineNumber = line.getLineNumber();
AddressSet addresses = getAddresses(function, line);
LocationReferenceContext context = builder.build();
TextMatch match =
new TextMatch(function, addresses, lineNumber, searchText, context, false);
callback.accept(match);
}
private AddressSet getAddresses(Function function, ClangLine line) {
Program program = function.getProgram();
AddressSpace space = function.getEntryPoint().getAddressSpace();
List<ClangToken> tokens = line.getAllTokens();
return DecompilerUtils.findClosestAddressSet(program, space, tokens);
}
/**
* A text line represents a ClangLine, it's pretty text, its character position in the
* overall body of text and the character positions of the portion of text that has matched
* a search. A line may have a search match that is partial or the entire line may match
* the search, such as in a multi-line match.
*/
private class TextLine {
private ClangLine cLine;
private int offset; // the character offset into the entire body of text
private String text;
// relative offsets
private int matchStart;
private int matchEnd;
TextLine(int offset, ClangLine cLine, String text) {
this.cLine = cLine;
this.offset = offset;
this.text = text;
matchStart = 0;
matchEnd = text.length();
}
ClangLine getCLine() {
return cLine;
}
int length() {
return text.length();
}
int getOffset() {
return offset;
}
int getEndOffset() {
return offset + length();
}
int getLineNumber() {
return cLine.getLineNumber();
}
String getText() {
return text;
}
int getMatchStart() {
return matchStart;
}
int getMatchEnd() {
return matchEnd;
}
void setMatchStart(int matchStart) {
this.matchStart = matchStart;
}
void setMatchEnd(int matchEnd) {
this.matchEnd = matchEnd;
}
@Override
public String toString() {
return Json.toString(this);
}
}
}
private static class DecompilerConfigurer implements DecompileConfigurer {
@Override
public void configure(DecompInterface decompiler) {
decompiler.toggleCCode(true);
decompiler.toggleSyntaxTree(true);
decompiler.setSimplificationStyle("decompile");
DecompileOptions xmlOptions = new DecompileOptions();
xmlOptions.setDefaultTimeout(60);
decompiler.setOptions(xmlOptions);
}
}
}

View File

@ -0,0 +1,235 @@
/* ###
* 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.plugin.core.search;
import java.awt.BorderLayout;
import java.awt.event.KeyEvent;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.apache.commons.lang3.StringUtils;
import docking.ReusableDialogComponentProvider;
import docking.widgets.checkbox.GCheckBox;
import docking.widgets.combobox.GhidraComboBox;
import docking.widgets.label.GLabel;
import ghidra.util.HelpLocation;
import ghidra.util.MessageType;
/**
* A dialog to gather input for performing a search over decompiled text.
*/
public class DecompilerTextFinderDialog extends ReusableDialogComponentProvider {
private GhidraComboBox<String> textCombo;
private JButton searchButton;
private JCheckBox regexCb;
private JCheckBox searchSelectionCb;
private String searchText;
private boolean isCancelled;
public DecompilerTextFinderDialog() {
super("Decompiled Function Search");
addWorkPanel(buildMainPanel());
buildButtons();
setHelpLocation(new HelpLocation("DecompilerTextFinderPlugin", "Search_Decompiled_Text"));
}
private void buildButtons() {
searchButton = new JButton("Search");
searchButton.addActionListener(ev -> doSearch());
addButton(searchButton);
setDefaultButton(searchButton);
addCancelButton();
}
private JPanel buildMainPanel() {
regexCb = new GCheckBox("Regular Expression", false);
regexCb.setName("Regular Expression Search");
regexCb.addItemListener(e -> clearStatusText());
searchSelectionCb = new JCheckBox("Search Selection");
searchSelectionCb.setName("Search Selection");
textCombo = new GhidraComboBox<>();
textCombo.setEditable(true);
textCombo.addActionListener(e -> doSearch());
textCombo.setColumns(20);
textCombo.addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent e) {
handleDocumentUpdate();
}
@Override
public void insertUpdate(DocumentEvent e) {
handleDocumentUpdate();
}
@Override
public void removeUpdate(DocumentEvent e) {
handleDocumentUpdate();
}
private void handleDocumentUpdate() {
String text = textCombo.getText();
searchButton.setEnabled(!StringUtils.isBlank(text));
clearStatusText();
}
});
JLabel findLabel = new GLabel("Find: ");
// associate this label with a mnemonic key that activates the text field
findLabel.setDisplayedMnemonic(KeyEvent.VK_N);
textCombo.associateLabel(findLabel);
JPanel mainPanel = new JPanel(new BorderLayout());
JPanel textPanel = new JPanel();
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.LINE_AXIS));
textPanel.add(findLabel);
textPanel.add(textCombo);
mainPanel.add(textPanel, BorderLayout.NORTH);
mainPanel.add(buildOptionsPanel(), BorderLayout.SOUTH);
mainPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
return mainPanel;
}
private JPanel buildOptionsPanel() {
JPanel optionsPanel = new JPanel();
optionsPanel.setLayout(new BoxLayout(optionsPanel, BoxLayout.LINE_AXIS));
optionsPanel.setBorder(BorderFactory.createTitledBorder("Options"));
optionsPanel.add(regexCb);
optionsPanel.add(Box.createHorizontalGlue());
optionsPanel.add(searchSelectionCb);
return optionsPanel;
}
@Override
public void close() {
textCombo.setText("");
super.close();
}
private void doSearch() {
searchText = null;
clearStatusText();
if (!searchButton.isEnabled()) {
return; // don't search while disabled
}
String text = textCombo.getText();
if (!validateRegex(text)) {
return; // leave the dialog open so the user can see the error text
}
isCancelled = false;
searchText = text;
updateSearchHistory(searchText);
close();
}
private boolean validateRegex(String text) {
if (!isRegex()) {
return true;
}
try {
Pattern.compile(text);
return true;
}
catch (PatternSyntaxException e) {
setStatusText("Invalid regex: " + e.getMessage(), MessageType.ERROR);
return false;
}
}
@Override
protected void dialogShown() {
searchButton.setEnabled(false);
clearStatusText();
searchText = null;
// To track cancelled, assume that the dialog is always in a cancelled state unless the
// user actually performed a search.
isCancelled = true;
}
public void setSearchText(String text) {
textCombo.setText(text);
}
public String getSearchText() {
return searchText;
}
public boolean isSearchSelection() {
return searchSelectionCb.isSelected();
}
public void setSearchSelectionEnabled(boolean b) {
if (!b) {
searchSelectionCb.setEnabled(false);
searchSelectionCb.setSelected(false);
}
else {
searchSelectionCb.setEnabled(true);
}
}
public boolean isRegex() {
return regexCb.isSelected();
}
public boolean isCancelled() {
return isCancelled;
}
private void updateSearchHistory(String text) {
MutableComboBoxModel<String> model = (MutableComboBoxModel<String>) textCombo.getModel();
model.insertElementAt(text, 0);
int size = model.getSize();
for (int i = 1; i < size; i++) {
String element = model.getElementAt(i);
if (element.equals(text)) { // already in the list, remove it
model.removeElementAt(i);
break;
}
}
// do this last since removing items may change the selected item
model.setSelectedItem(text);
}
}

View File

@ -0,0 +1,167 @@
/* ###
* 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.plugin.core.search;
import java.awt.Color;
import javax.swing.Icon;
import docking.action.MenuData;
import docking.action.builder.ActionBuilder;
import generic.theme.GIcon;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.NavigatableActionContext;
import ghidra.app.context.NavigatableContextAction;
import ghidra.app.events.ProgramSelectionPluginEvent;
import ghidra.app.nav.Navigatable;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.plugin.ProgramPlugin;
import ghidra.app.plugin.core.table.TableComponentProvider;
import ghidra.app.util.SearchConstants;
import ghidra.app.util.query.TableService;
import ghidra.framework.options.ToolOptions;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.*;
import ghidra.program.util.ProgramSelection;
import ghidra.util.table.GhidraTable;
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = CorePluginPackage.NAME,
category = PluginCategoryNames.SEARCH,
shortDescription = "Decompiler Text Finder Plugin",
description = "This plugin adds an action to allow users to search decompiled text.",
servicesRequired = { TableService.class }
)
//@formatter:on
public class DecompilerTextFinderPlugin extends ProgramPlugin {
private static final Icon SELECT_FUNCTIONS_ICON =
new GIcon("icon.plugin.decompiler.text.finder.select.functions");
private DecompilerTextFinderDialog searchDialog;
public DecompilerTextFinderPlugin(PluginTool tool) {
super(tool);
createActions();
}
private void createActions() {
NavigatableContextAction searchAction =
new NavigatableContextAction("Search Decompiled Text", getName()) {
@Override
public void actionPerformed(NavigatableActionContext context) {
search(context.getNavigatable(), context.getProgram());
}
};
// Memory Search uses groups 'a', 'b', and 'c'; Search Text uses group 'd'
String subGroup = "e";
searchAction.setMenuBarData(new MenuData(new String[] { "Search", "Decompiled Text..." },
null, "search", -1, subGroup));
searchAction.addToWindowWhen(NavigatableActionContext.class);
tool.addAction(searchAction);
}
private void search(Navigatable navigatable, Program program) {
if (searchDialog == null) {
searchDialog = new DecompilerTextFinderDialog();
}
// update the Search Selection checkbox as needed
boolean enableSearchSelection = false;
if (currentSelection != null) {
FunctionManager functionManager = program.getFunctionManager();
FunctionIterator it = functionManager.getFunctions(currentSelection, true);
if (it.hasNext()) {
enableSearchSelection = true;
}
}
searchDialog.setSearchSelectionEnabled(enableSearchSelection);
tool.showDialog(searchDialog);
if (searchDialog.isCancelled()) {
return;
}
String searchText = searchDialog.getSearchText();
boolean isRegex = searchDialog.isRegex();
String title = "Decompiler Search Text - '" + searchText + "'";
String tableTypeName = "Decompiler Search";
DecompilerTextFinderTableModel model =
new DecompilerTextFinderTableModel(tool, program, searchText, isRegex);
if (searchDialog.isSearchSelection()) {
model.setSelection(currentSelection);
}
int searchLimit = getSearchLimit();
model.setSearchLimit(searchLimit);
Color markerColor = SearchConstants.SEARCH_HIGHLIGHT_COLOR;
Icon markerIcon = new GIcon("icon.base.search.marker");
String windowSubMenu = "Search";
TableService tableService = tool.getService(TableService.class);
TableComponentProvider<TextMatch> provider = tableService.showTableWithMarkers(title,
tableTypeName, model, markerColor, markerIcon, windowSubMenu, navigatable);
provider.installRemoveItemsAction();
GhidraTable table = provider.getTable();
//@formatter:off
new ActionBuilder("Select Functions", getName())
.description("Make program selection of function starts from selected rows")
.toolBarIcon(SELECT_FUNCTIONS_ICON)
.popupMenuIcon(SELECT_FUNCTIONS_ICON)
.popupMenuPath("Select Functions")
.popupMenuGroup(null, "a") // before the others in the table, to match the toolbar
.enabledWhen(c -> table.getSelectedRowCount() > 0)
.onAction(c -> selectFunctions(table, model))
.buildAndInstallLocal(provider)
;
//@formatter:on
}
private int getSearchLimit() {
ToolOptions options = tool.getOptions(SearchConstants.SEARCH_OPTION_NAME);
return options.getInt(SearchConstants.SEARCH_LIMIT_NAME,
SearchConstants.DEFAULT_SEARCH_LIMIT);
}
private void selectFunctions(GhidraTable table, DecompilerTextFinderTableModel model) {
AddressSet addresses = new AddressSet();
int[] rows = table.getSelectedRows();
for (int row : rows) {
TextMatch match = model.getRowObject(row);
Function f = match.getFunction();
addresses.add(f.getEntryPoint());
}
ProgramSelection selection = new ProgramSelection(addresses);
PluginEvent event =
new ProgramSelectionPluginEvent(getName(), selection, table.getProgram());
firePluginEvent(event);
}
}

View File

@ -0,0 +1,200 @@
/* ###
* 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.plugin.core.search;
import java.awt.Component;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import docking.widgets.table.*;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.*;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.GhidraProgramTableModel;
import ghidra.util.table.column.AbstractGhidraColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.table.field.AbstractProgramBasedDynamicTableColumn;
import ghidra.util.table.field.FunctionNameTableColumn;
import ghidra.util.task.TaskMonitor;
public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<TextMatch> {
private String searchText;
private boolean isRegex;
private AddressSetView selection;
private int searchLimit;
protected DecompilerTextFinderTableModel(ServiceProvider serviceProvider, Program program,
String searchText, boolean isRegex) {
super("Decompiler Search", serviceProvider, program, null, true);
this.searchText = searchText;
this.isRegex = isRegex;
}
void setSelection(AddressSetView selection) {
this.selection = selection;
}
void setSearchLimit(int limit) {
this.searchLimit = limit;
}
@Override
protected TableColumnDescriptor<TextMatch> createTableColumnDescriptor() {
TableColumnDescriptor<TextMatch> descriptor = new TableColumnDescriptor<>();
descriptor.addVisibleColumn(
DiscoverableTableUtils.adaptColumForModel(this, new FunctionNameTableColumn()), 1,
true);
descriptor.addVisibleColumn(new LineNumberTableColumn(), 2, true);
descriptor.addVisibleColumn(new ContextTableColumn());
return descriptor;
}
@Override
protected void doLoad(Accumulator<TextMatch> accumulator, TaskMonitor monitor)
throws CancelledException {
// Add a consumer that will monitor the count and stop the process on too many results
AtomicInteger counter = new AtomicInteger();
Consumer<TextMatch> limitedConsumer = tm -> {
int count = counter.incrementAndGet();
if (count >= searchLimit) {
monitor.cancel();
}
accumulator.add(tm);
};
Pattern pattern;
if (isRegex) {
// note: we expect this to be a valid regex
pattern = Pattern.compile(searchText);
}
else {
String quoted = Pattern.quote(searchText);
pattern = Pattern.compile(quoted, Pattern.CASE_INSENSITIVE);
}
DecompilerTextFinder finder = new DecompilerTextFinder();
if (selection != null) {
FunctionManager functionManager = program.getFunctionManager();
FunctionIterator functions = functionManager.getFunctions(selection, true);
finder.findText(program, pattern, functions, limitedConsumer, monitor);
}
else {
finder.findText(program, pattern, limitedConsumer, monitor);
}
}
@Override
public Address getAddress(int row) {
TextMatch match = getRowObject(row);
if (match != null) {
return match.getAddress();
}
return null;
}
//=================================================================================================
//Inner Classes
//=================================================================================================
private class LineNumberTableColumn
extends AbstractProgramBasedDynamicTableColumn<TextMatch, Integer> {
@Override
public Integer getValue(TextMatch rowObject, Settings settings, Program p,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getLineNumber();
}
@Override
public String getColumnName() {
return "Line Number";
}
@Override
public int getColumnPreferredWidth() {
return 75;
}
}
private class ContextTableColumn
extends AbstractProgramBasedDynamicTableColumn<TextMatch, LocationReferenceContext> {
private ContextCellRenderer renderer = new ContextCellRenderer();
@Override
public LocationReferenceContext getValue(TextMatch rowObject, Settings settings, Program p,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getContext();
}
@Override
public String getColumnName() {
return "Context";
}
@Override
public GColumnRenderer<LocationReferenceContext> getColumnRenderer() {
return renderer;
}
}
private class ContextCellRenderer
extends AbstractGhidraColumnRenderer<LocationReferenceContext> {
{
// the context uses html
setHTMLRenderingEnabled(true);
}
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
// initialize
super.getTableCellRendererComponent(data);
TextMatch match = (TextMatch) data.getRowObject();
LocationReferenceContext context = match.getContext();
String text;
if (match.isMultiLine()) {
// multi-line matches create visual noise when showing colors, as of much of the
// entire line matches
text = context.getPlainText();
}
else {
text = context.getBoldMatchingText();
}
setText(text);
return this;
}
@Override
public String getFilterString(LocationReferenceContext context, Settings settings) {
return context.getPlainText();
}
}
}

View File

@ -0,0 +1,79 @@
/* ###
* 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.plugin.core.search;
import generic.json.Json;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.Function;
/**
* A simple class that represents text found in Decompiler output.
*/
public class TextMatch {
private Function function;
private AddressSet addresses;
private LocationReferenceContext context;
private int lineNumber;
private String searchText;
private boolean isMultiLine;
TextMatch(Function function, AddressSet addresses, int lineNumber, String searchText,
LocationReferenceContext context, boolean isMultiLine) {
this.function = function;
this.addresses = addresses;
this.lineNumber = lineNumber;
this.searchText = searchText;
this.context = context;
this.isMultiLine = isMultiLine;
}
public Function getFunction() {
return function;
}
public LocationReferenceContext getContext() {
return context;
}
public int getLineNumber() {
return lineNumber;
}
public Address getAddress() {
if (addresses.isEmpty()) {
return function.getEntryPoint();
}
return addresses.getFirstRange().getMinAddress();
}
public boolean isMultiLine() {
return isMultiLine;
}
public String getSearchText() {
return searchText;
}
@Override
public String toString() {
return Json.toString(this, "function", "context", "searchText");
}
}

View File

@ -0,0 +1,30 @@
/* ###
* 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.plugin.core.search;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
import ghidra.program.model.listing.Program;
import ghidra.util.table.ProgramLocationTableRowMapper;
public class TextMatchToAddressTableRowMapper
extends ProgramLocationTableRowMapper<TextMatch, Address> {
@Override
public Address map(TextMatch rowObject, Program data, ServiceProvider serviceProvider) {
return rowObject.getAddress();
}
}

View File

@ -0,0 +1,30 @@
/* ###
* 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.plugin.core.search;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.util.table.ProgramLocationTableRowMapper;
public class TextMatchToFunctionTableRowMapper
extends ProgramLocationTableRowMapper<TextMatch, Function> {
@Override
public Function map(TextMatch rowObject, Program data, ServiceProvider serviceProvider) {
return rowObject.getFunction();
}
}

View File

@ -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.
@ -23,7 +23,8 @@ import org.junit.Before;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.program.database.*;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.database.data.ProgramDataTypeManager;
import ghidra.program.model.data.*;
@ -81,14 +82,14 @@ public class FormatStringParserTest extends AbstractGenericTest {
runFormatTest("%d%hi", expectedTypes2, false);
DataType[] expectedTypes3 =
{ program.getDataTypeManager().getPointer(new PointerDataType(DataType.VOID)),
{ program.getDataTypeManager().getPointer(new PointerDataType(VoidDataType.dataType)),
program.getDataTypeManager().getPointer(new CharDataType()) };
runFormatTest("%p%*d%s", expectedTypes3, false);
DataType[] expectedTypes4 =
{ program.getDataTypeManager().getPointer(new LongDoubleDataType()),
program.getDataTypeManager().getPointer(new CharDataType()),
program.getDataTypeManager().getPointer(new PointerDataType(DataType.VOID)) };
DataType[] expectedTypes4 = {
program.getDataTypeManager().getPointer(new LongDoubleDataType()),
program.getDataTypeManager().getPointer(new CharDataType()),
program.getDataTypeManager().getPointer(new PointerDataType(VoidDataType.dataType)) };
runFormatTest("!:%12La%*d+=%2s%3p%*20d", expectedTypes4, false);
@ -106,8 +107,8 @@ public class FormatStringParserTest extends AbstractGenericTest {
{ program.getDataTypeManager().getPointer(new CharDataType()), new LongDataType() };
runFormatTest("#thisisatest%+-4.12s%#.1lin\nd2", expectedTypes2, true);
DataType[] expectedTypes3 = { new PointerDataType(DataType.VOID), new LongDoubleDataType(),
new UnsignedCharDataType() };
DataType[] expectedTypes3 = { new PointerDataType(VoidDataType.dataType),
new LongDoubleDataType(), new UnsignedCharDataType() };
runFormatTest("%01.3pp%%%#1.2Lg%%%%%hhXxn2", expectedTypes3, true);
DataType[] expectedTypes4 = { new IntegerDataType(), new IntegerDataType(),

View File

@ -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.
@ -501,12 +501,18 @@ public abstract class DockingAction implements DockingActionIf {
// menu path
if (menuBarData != null) {
buffer.append(" MENU PATH: ").append(
menuBarData.getMenuPathAsString());
buffer.append(" MENU PATH: ")
.append(menuBarData.getMenuPathAsString());
buffer.append('\n');
buffer.append(" MENU GROUP: ").append(menuBarData.getMenuGroup());
buffer.append('\n');
String menuSubGroup = menuBarData.getMenuSubGroup();
if (menuSubGroup != null) {
buffer.append(" MENU SUB-GROUP: ").append(menuSubGroup);
buffer.append('\n');
}
String parentGroup = menuBarData.getParentMenuGroup();
if (parentGroup != null) {
buffer.append(" PARENT GROUP: ").append(parentGroup);
@ -528,8 +534,8 @@ public abstract class DockingAction implements DockingActionIf {
// popup menu path
if (popupMenuData != null) {
buffer.append(" POPUP PATH: ").append(
popupMenuData.getMenuPathAsString());
buffer.append(" POPUP PATH: ")
.append(popupMenuData.getMenuPathAsString());
buffer.append('\n');
buffer.append(" POPUP GROUP: ").append(popupMenuData.getMenuGroup());
buffer.append('\n');

View File

@ -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.
@ -15,8 +15,11 @@
*/
package generic.json;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.lang3.builder.*;
@ -135,16 +138,14 @@ public class Json extends ToStringStyle {
* @return the string
*/
public static String toStringExclude(Object o, String... excludedFields) {
ReflectionToStringBuilder builder = new ReflectionToStringBuilder(o,
Json.WITH_NEWLINES);
ReflectionToStringBuilder builder = new ReflectionToStringBuilder(o, Json.WITH_NEWLINES);
builder.setExcludeFieldNames(excludedFields);
return builder.toString();
}
// Future: update this class to use the order of the included fields to be the printed ordered
private static class InclusiveReflectionToStringBuilder extends ReflectionToStringBuilder {
private String[] includedNames;
private String[] includedNames = new String[0];
public InclusiveReflectionToStringBuilder(Object object) {
super(object, WITH_NEWLINES);
@ -156,27 +157,73 @@ public class Json extends ToStringStyle {
return false;
}
if (this.includedNames != null &&
Arrays.binarySearch(this.includedNames, field.getName()) >= 0) {
return true;
if (includedNames.length == 0) {
return true; // this implies a programming error
}
String fieldName = field.getName();
for (String name : includedNames) {
if (fieldName.equals(name)) {
return true;
}
}
return false;
}
// Overridden to control the order the field are listed. The parent class sorts by name; we
// want output in the order specified by the user.
@Override
protected void appendFieldsIn(final Class<?> clazz) {
if (clazz.isArray()) {
super.appendFieldsIn(clazz);
return;
}
if (includedNames.length == 0) {
super.appendFieldsIn(clazz);
return;
}
Field[] fields = clazz.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
Map<String, Field> fieldsByName =
Arrays.stream(fields).collect(Collectors.toMap(f -> f.getName(), f -> f));
for (String name : includedNames) {
Field field = fieldsByName.get(name);
if (field == null) {
continue;
}
if (accept(field)) {
try {
// Field.get(Object) creates wrappers objects for primitive types.
Object fieldValue = this.getValue(field);
if (!isExcludeNullValues() || fieldValue != null) {
this.append(name, fieldValue,
!field.isAnnotationPresent(ToStringSummary.class));
}
}
catch (IllegalAccessException ex) {
throw new InternalError(
"Unexpected IllegalAccessException: " + ex.getMessage());
}
}
}
}
/**
* Sets the names to be included
* @param includeFieldNamesParam the names
* @return this builder
*/
public ReflectionToStringBuilder setIncludeFieldNames(
final String... includeFieldNamesParam) {
public ReflectionToStringBuilder setIncludeFieldNames(String... includeFieldNamesParam) {
if (includeFieldNamesParam == null) {
this.includedNames = null;
this.includedNames = new String[0];
}
else {
this.includedNames = includeFieldNamesParam;
Arrays.sort(this.includedNames);
}
return this;
}

View File

@ -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.
@ -16,6 +16,7 @@
package ghidra.util.datastruct;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@ -31,7 +32,7 @@ import java.util.stream.StreamSupport;
*
* @param <T> the type
*/
public interface Accumulator<T> extends Iterable<T> {
public interface Accumulator<T> extends Iterable<T>, Consumer<T> {
public void add(T t);
@ -50,4 +51,9 @@ public interface Accumulator<T> extends Iterable<T> {
default Stream<T> stream() {
return StreamSupport.stream(spliterator(), false);
}
@Override
default void accept(T t) {
add(t);
}
}

View File

@ -305,10 +305,11 @@ public class UserSearchUtils {
*/
private static String convertUserInputToRegex(String input, boolean allowGlobbing) {
// Note: Order is important! (due to how escape characters added and checked)
String escaped = escapeEscapeCharacters(input);
String escaped = input;
if (allowGlobbing) {
// Note: Order is important! (due to how escape characters added and checked)
escaped = escapeEscapeCharacters(input);
escaped = escapeNonGlobbingRegexCharacters(input);
escaped = convertGlobbingCharactersToRegex(escaped);
}

View File

@ -0,0 +1,59 @@
/* ###
* 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 help.screenshot;
import org.junit.Test;
import docking.action.DockingActionIf;
import ghidra.app.plugin.core.search.DecompilerTextFinderDialog;
import ghidra.app.plugin.core.search.TextMatch;
import ghidra.app.plugin.core.table.TableComponentProvider;
import ghidra.util.table.GhidraProgramTableModel;
public class DecompilerTextFinderPluginScreenShots extends GhidraScreenShotGenerator {
@Test
public void testDecompilerTextFinderDialog() {
DockingActionIf action = getAction(tool, "Search Decompiled Text");
performAction(action, false);
captureDialog();
closeAllWindows();
}
@Test
public void testDecompilerTextFinderResultsTable() {
DockingActionIf action = getAction(tool, "Search Decompiled Text");
performAction(action, false);
DecompilerTextFinderDialog searchDialog =
waitForDialogComponent(DecompilerTextFinderDialog.class);
String searchText = " = '\\0'";
runSwing(() -> searchDialog.setSearchText(searchText));
pressButtonByText(searchDialog, "Search", false);
@SuppressWarnings("unchecked")
TableComponentProvider<TextMatch> tableProvider =
waitForComponentProvider(TableComponentProvider.class);
GhidraProgramTableModel<TextMatch> model = tableProvider.getModel();
waitForTableModel(model);
// TOD capture entire window?
captureProvider(tableProvider);
close(searchDialog);
}
}