Skip to content

Commit 7f7d3e0

Browse files
committed
Add instruction mnemonic search
Allows searching against disassembled method instructions
1 parent 9021447 commit 7f7d3e0

File tree

6 files changed

+262
-1
lines changed

6 files changed

+262
-1
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package software.coley.recaf.services.search.query;
2+
3+
import jakarta.annotation.Nonnull;
4+
import jakarta.annotation.Nullable;
5+
import org.objectweb.asm.ClassReader;
6+
import org.objectweb.asm.tree.ClassNode;
7+
import org.objectweb.asm.tree.MethodNode;
8+
import software.coley.recaf.path.ClassMemberPathNode;
9+
import software.coley.recaf.path.InstructionPathNode;
10+
import software.coley.recaf.services.search.JvmClassSearchVisitor;
11+
import software.coley.recaf.services.search.match.StringPredicate;
12+
import software.coley.recaf.util.BlwUtil;
13+
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
17+
/**
18+
* Instruction text search implementation.
19+
*
20+
* @author Matt Coley
21+
*/
22+
public class InstructionQuery implements JvmClassQuery {
23+
private final List<StringPredicate> predicates;
24+
25+
/**
26+
* @param predicates
27+
* List of predicates, where each entry matches a single line of disassembled instruction text.
28+
*/
29+
public InstructionQuery(@Nonnull List<StringPredicate> predicates) {
30+
this.predicates = predicates;
31+
}
32+
33+
@Nonnull
34+
@Override
35+
public JvmClassSearchVisitor visitor(@Nullable JvmClassSearchVisitor delegate) {
36+
return (resultSink, classPath, classInfo) -> {
37+
ClassNode node = new ClassNode();
38+
classInfo.getClassReader().accept(node, ClassReader.SKIP_FRAMES);
39+
List<String> matched = new ArrayList<>(predicates.size());
40+
for (MethodNode method : node.methods) {
41+
if (method.instructions == null)
42+
continue;
43+
ClassMemberPathNode memberPath = classPath.child(method.name, method.desc);
44+
if (memberPath == null)
45+
continue;
46+
matched.clear();
47+
for (int i = 0; i < method.instructions.size() - predicates.size(); i++) {
48+
for (int j = 0; j < predicates.size(); j++) {
49+
int line = i + j;
50+
51+
// This utility call maps instructions to BLW ones, and passes them to JASM
52+
// so the format should match what you see in the assembler, barring labels
53+
// and other debug info.
54+
String disassembled = BlwUtil.toString(method.instructions.get(line));
55+
if (!predicates.get(j).match(disassembled)) {
56+
matched.clear();
57+
break;
58+
} else {
59+
matched.add(disassembled);
60+
}
61+
}
62+
63+
// Add result if we matched all predicates.
64+
if (matched.size() == predicates.size()) {
65+
InstructionPathNode path = memberPath.childInsn(method.instructions.get(i), i);
66+
resultSink.accept(path, String.join("\n", matched));
67+
matched.clear();
68+
}
69+
}
70+
}
71+
};
72+
}
73+
}

recaf-core/src/test/java/software/coley/recaf/services/search/SearchServiceTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import software.coley.recaf.services.search.match.NumberPredicateProvider;
1818
import software.coley.recaf.services.search.match.StringPredicateProvider;
1919
import software.coley.recaf.services.search.query.DeclarationQuery;
20+
import software.coley.recaf.services.search.query.InstructionQuery;
2021
import software.coley.recaf.services.search.query.NumberQuery;
2122
import software.coley.recaf.services.search.query.Query;
2223
import software.coley.recaf.services.search.query.ReferenceQuery;
@@ -36,6 +37,7 @@
3637

3738
import java.io.IOException;
3839
import java.nio.charset.StandardCharsets;
40+
import java.util.List;
3941
import java.util.Set;
4042
import java.util.stream.Collectors;
4143

@@ -130,6 +132,16 @@ void testClassStrings() {
130132
assertEquals(1, results.size());
131133
}
132134

135+
@Test
136+
void testInsnSearch() {
137+
Results results = searchService.search(classesWorkspace, new InstructionQuery(List.of(
138+
strMatchProvider.newEqualPredicate("getstatic java/lang/System.out Ljava/io/PrintStream;"),
139+
strMatchProvider.newEqualPredicate("ldc \"Hello world\""),
140+
strMatchProvider.newEqualPredicate("invokevirtual java/io/PrintStream.println (Ljava/lang/String;)V")
141+
)));
142+
assertEquals(1, results.size());
143+
}
144+
133145
@Test
134146
void testFieldPath() {
135147
// Used only in constant-value attribute for field 'CONSTANT_FIELD'

recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
import software.coley.recaf.ui.pane.editing.text.TextFilePane;
9494
import software.coley.recaf.ui.pane.search.AbstractSearchPane;
9595
import software.coley.recaf.ui.pane.search.ClassReferenceSearchPane;
96+
import software.coley.recaf.ui.pane.search.InstructionSearchPane;
9697
import software.coley.recaf.ui.pane.search.MemberDeclarationSearchPane;
9798
import software.coley.recaf.ui.pane.search.MemberReferenceSearchPane;
9899
import software.coley.recaf.ui.pane.search.NumberSearchPane;
@@ -176,6 +177,7 @@ public class Actions implements Service {
176177
private final Instance<ClassReferenceSearchPane> classReferenceSearchPaneProvider;
177178
private final Instance<MemberReferenceSearchPane> memberReferenceSearchPaneProvider;
178179
private final Instance<MemberDeclarationSearchPane> memberDeclarationSearchPaneProvider;
180+
private final Instance<InstructionSearchPane> instructionSearchPaneProvider;
179181
private final KeybindingConfig keybindingConfig;
180182
private final ActionsConfig config;
181183

@@ -210,7 +212,8 @@ public Actions(@Nonnull ActionsConfig config,
210212
@Nonnull Instance<MethodCallGraphsPane> callGraphsPaneProvider,
211213
@Nonnull Instance<ClassReferenceSearchPane> classReferenceSearchPaneProvider,
212214
@Nonnull Instance<MemberReferenceSearchPane> memberReferenceSearchPaneProvider,
213-
@Nonnull Instance<MemberDeclarationSearchPane> memberDeclarationSearchPaneProvider) {
215+
@Nonnull Instance<MemberDeclarationSearchPane> memberDeclarationSearchPaneProvider,
216+
@Nonnull Instance<InstructionSearchPane> instructionSearchPaneProvider) {
214217
this.config = config;
215218
this.keybindingConfig = keybindingConfig;
216219
this.workspaceManager = workspaceManager;
@@ -241,6 +244,7 @@ public Actions(@Nonnull ActionsConfig config,
241244
this.classReferenceSearchPaneProvider = classReferenceSearchPaneProvider;
242245
this.memberReferenceSearchPaneProvider = memberReferenceSearchPaneProvider;
243246
this.memberDeclarationSearchPaneProvider = memberDeclarationSearchPaneProvider;
247+
this.instructionSearchPaneProvider = instructionSearchPaneProvider;
244248
}
245249

246250
/**
@@ -2419,6 +2423,14 @@ public MemberDeclarationSearchPane openNewMemberDeclarationSearch() {
24192423
return openSearchPane("menu.search.class.member-declarations", CarbonIcons.CODE, memberDeclarationSearchPaneProvider);
24202424
}
24212425

2426+
/**
2427+
* @return New instruction search pane, opened in a new docking tab.
2428+
*/
2429+
@Nonnull
2430+
public InstructionSearchPane openNewInstructionSearch() {
2431+
return openSearchPane("menu.search.class.instruction", CarbonIcons.CODE, instructionSearchPaneProvider);
2432+
}
2433+
24222434
@Nonnull
24232435
private <T extends AbstractSearchPane> T openSearchPane(@Nonnull String titleId, @Nonnull Ikon icon, @Nonnull Instance<T> paneProvider) {
24242436
// Place the tab in a region with other comments if possible.

recaf-ui/src/main/java/software/coley/recaf/ui/menubar/SearchMenu.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public SearchMenu(@Nonnull WorkspaceManager workspaceManager,
3232
getItems().add(action("menu.search.class.type-references", CarbonIcons.CODE_REFERENCE, actions::openNewClassReferenceSearch));
3333
getItems().add(action("menu.search.class.member-references", CarbonIcons.CODE_REFERENCE, actions::openNewMemberReferenceSearch));
3434
getItems().add(action("menu.search.class.member-declarations", CarbonIcons.CODE, actions::openNewMemberDeclarationSearch));
35+
getItems().add(action("menu.search.class.instruction", CarbonIcons.CODE, actions::openNewInstructionSearch));
3536

3637
disableProperty().bind(hasWorkspace.not());
3738
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package software.coley.recaf.ui.pane.search;
2+
3+
import jakarta.annotation.Nonnull;
4+
import jakarta.annotation.Nullable;
5+
import jakarta.enterprise.context.Dependent;
6+
import jakarta.inject.Inject;
7+
import javafx.beans.property.SimpleStringProperty;
8+
import javafx.beans.property.StringProperty;
9+
import javafx.collections.FXCollections;
10+
import javafx.collections.ListChangeListener;
11+
import javafx.collections.ObservableList;
12+
import javafx.geometry.Insets;
13+
import javafx.geometry.Pos;
14+
import javafx.scene.Node;
15+
import javafx.scene.control.Button;
16+
import javafx.scene.control.ComboBox;
17+
import javafx.scene.control.TextField;
18+
import javafx.scene.layout.ColumnConstraints;
19+
import javafx.scene.layout.GridPane;
20+
import javafx.scene.layout.Priority;
21+
import org.kordamp.ikonli.carbonicons.CarbonIcons;
22+
import org.reactfx.EventStreams;
23+
import software.coley.collections.Lists;
24+
import software.coley.recaf.services.cell.CellConfigurationService;
25+
import software.coley.recaf.services.navigation.Actions;
26+
import software.coley.recaf.services.search.SearchService;
27+
import software.coley.recaf.services.search.match.StringPredicate;
28+
import software.coley.recaf.services.search.match.StringPredicateProvider;
29+
import software.coley.recaf.services.search.query.InstructionQuery;
30+
import software.coley.recaf.services.search.query.Query;
31+
import software.coley.recaf.services.workspace.WorkspaceManager;
32+
import software.coley.recaf.ui.control.ActionButton;
33+
import software.coley.recaf.ui.control.BoundBiDiComboBox;
34+
import software.coley.recaf.ui.control.richtext.Editor;
35+
import software.coley.recaf.util.Lang;
36+
import software.coley.recaf.util.RegexUtil;
37+
import software.coley.recaf.util.ToStringConverter;
38+
39+
import java.time.Duration;
40+
import java.util.Collections;
41+
import java.util.List;
42+
import java.util.Objects;
43+
import java.util.UUID;
44+
45+
import static software.coley.recaf.services.search.match.StringPredicateProvider.KEY_ANYTHING;
46+
import static software.coley.recaf.services.search.match.StringPredicateProvider.KEY_NOTHING;
47+
48+
/**
49+
* Instruction disassembly search pane.
50+
*
51+
* @author Matt Coley
52+
*/
53+
@Dependent
54+
public class InstructionSearchPane extends AbstractSearchPane {
55+
private final StringPredicateProvider stringPredicateProvider;
56+
private final ObservableList<Line> lines = FXCollections.observableArrayList();
57+
58+
@Inject
59+
public InstructionSearchPane(@Nonnull WorkspaceManager workspaceManager,
60+
@Nonnull SearchService searchService,
61+
@Nonnull CellConfigurationService configurationService,
62+
@Nonnull Actions actions,
63+
@Nonnull StringPredicateProvider stringPredicateProvider) {
64+
super(workspaceManager, searchService, configurationService, actions);
65+
66+
this.stringPredicateProvider = stringPredicateProvider;
67+
68+
GridPane input = new GridPane();
69+
ColumnConstraints colTexts = new ColumnConstraints();
70+
ColumnConstraints colCombos = new ColumnConstraints();
71+
colTexts.setFillWidth(true);
72+
input.setAlignment(Pos.CENTER);
73+
input.getColumnConstraints().addAll(colTexts, colCombos);
74+
input.setHgap(10);
75+
input.setPadding(new Insets(10));
76+
77+
List<String> stringPredicates = stringPredicateProvider.getBiStringMatchers().keySet().stream()
78+
.filter(s -> !KEY_ANYTHING.equals(s) && !KEY_NOTHING.equals(s))
79+
.sorted().toList();
80+
lines.addListener((ListChangeListener<Line>) change -> {
81+
while (change.next()) {
82+
for (Line line : change.getAddedSubList()) {
83+
StringProperty stringValue = line.text();
84+
StringProperty stringPredicateId = line.predicateId();
85+
TextField textField = new TextField();
86+
textField.setId(line.uuid().toString());
87+
stringValue.bind(textField.textProperty());
88+
ComboBox<String> modeCombo = new BoundBiDiComboBox<>(stringPredicateId, stringPredicates,
89+
ToStringConverter.from(s -> Lang.get(StringPredicate.TRANSLATION_PREFIX + s)));
90+
modeCombo.getSelectionModel().select(StringPredicateProvider.KEY_CONTAINS);
91+
EventStreams.changesOf(stringValue)
92+
.or(EventStreams.changesOf(stringPredicateId))
93+
.reduceSuccessions(Collections::singletonList, Lists::add, Duration.ofMillis(Editor.MEDIUM_DELAY_MS))
94+
.addObserver(unused -> search());
95+
GridPane.setHgrow(textField, Priority.ALWAYS);
96+
input.addRow(input.getRowCount(), textField, modeCombo, new ActionButton(CarbonIcons.TRASH_CAN, () -> lines.remove(line)));
97+
}
98+
for (Line line : change.getRemoved()) {
99+
for (Node child : input.getChildrenUnmodifiable()) {
100+
if (line.uuid().toString().equals(child.getId())) {
101+
Integer nodeRow = GridPane.getRowIndex(child);
102+
if (nodeRow != null) {
103+
input.getRowConstraints().remove(nodeRow.intValue());
104+
break;
105+
}
106+
}
107+
}
108+
}
109+
}
110+
});
111+
112+
// Add an initial line, and a button to facilitate adding additional lines
113+
lines.add(new Line(UUID.randomUUID(), new SimpleStringProperty(), new SimpleStringProperty()));
114+
Button addLine = new ActionButton(CarbonIcons.ADD_ALT, Lang.getBinding("dialog.search.add-instruction-line"), () -> {
115+
lines.add(new Line(UUID.randomUUID(), new SimpleStringProperty(), new SimpleStringProperty()));
116+
});
117+
input.addRow(0, addLine);
118+
setInputs(input);
119+
}
120+
121+
/**
122+
* @return List of models to match against lines of disassembled instructions.
123+
*/
124+
@Nonnull
125+
public ObservableList<Line> getLinePredicates() {
126+
return lines;
127+
}
128+
129+
@Nullable
130+
@Override
131+
protected Query buildQuery() {
132+
List<StringPredicate> predicates = lines.stream().map(line -> {
133+
String search = line.text().get();
134+
String id = line.predicateId().get();
135+
136+
// Skip for blank input
137+
if (search.isBlank())
138+
return null;
139+
140+
// Validate regex input
141+
if (id.contains("regex") && !RegexUtil.validate(search).valid())
142+
return null;
143+
144+
// May be null if no such id exists as a predicate, but we filter out nulls anyways.
145+
return stringPredicateProvider.newBiStringPredicate(id, search);
146+
}).filter(Objects::nonNull).toList();
147+
return new InstructionQuery(predicates);
148+
}
149+
150+
/**
151+
* Model of a single line of text to search against disassembled code.
152+
*
153+
* @param uuid
154+
* Unique identifier for this input.
155+
* @param text
156+
* Text to match on the line.
157+
* @param predicateId
158+
* Predicate identifier for {@link StringPredicateProvider} lookups.
159+
*/
160+
public record Line(@Nonnull UUID uuid, @Nonnull StringProperty text, @Nonnull StringProperty predicateId) {}
161+
}

recaf-ui/src/main/resources/translations/en_US.lang

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ menu.search.number=Numbers
7474
menu.search.class.member-declarations=Member declarations
7575
menu.search.class.member-references=Member references
7676
menu.search.class.type-references=Type references
77+
menu.search.class.instruction=Instruction disassembly
7778
menu.search.method-overrides=Method overrides
7879
menu.search.method-references=Method references
7980
menu.search.field-references=Field references
@@ -165,6 +166,7 @@ dialog.search.type=Type name
165166
dialog.search.member-owner=Member owner type
166167
dialog.search.member-name=Member name
167168
dialog.search.member-descriptor=Member descriptor
169+
dialog.search.add-instruction-line=Add instruction line
168170

169171
## File chooser
170172
dialog.title.primary=Primary resource

0 commit comments

Comments
 (0)