summaryrefslogtreecommitdiff
path: root/src/main/java/cuchaz/enigma/gui/ClassMatchingGui.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/cuchaz/enigma/gui/ClassMatchingGui.java')
-rw-r--r--src/main/java/cuchaz/enigma/gui/ClassMatchingGui.java546
1 files changed, 546 insertions, 0 deletions
diff --git a/src/main/java/cuchaz/enigma/gui/ClassMatchingGui.java b/src/main/java/cuchaz/enigma/gui/ClassMatchingGui.java
new file mode 100644
index 0000000..ec63900
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/gui/ClassMatchingGui.java
@@ -0,0 +1,546 @@
1/*******************************************************************************
2 * Copyright (c) 2015 Jeff Martin.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the GNU Lesser General Public
5 * License v3.0 which accompanies this distribution, and is available at
6 * http://www.gnu.org/licenses/lgpl.html
7 * <p>
8 * Contributors:
9 * Jeff Martin - initial API and implementation
10 ******************************************************************************/
11package cuchaz.enigma.gui;
12
13import com.google.common.collect.BiMap;
14import com.google.common.collect.Lists;
15import com.google.common.collect.Maps;
16
17import java.awt.BorderLayout;
18import java.awt.Container;
19import java.awt.Dimension;
20import java.awt.FlowLayout;
21import java.awt.event.ActionListener;
22import java.util.Collection;
23import java.util.Collections;
24import java.util.List;
25import java.util.Map;
26
27import javax.swing.*;
28
29import cuchaz.enigma.Constants;
30import cuchaz.enigma.Deobfuscator;
31import cuchaz.enigma.convert.*;
32import cuchaz.enigma.gui.node.ClassSelectorClassNode;
33import cuchaz.enigma.gui.node.ClassSelectorPackageNode;
34import cuchaz.enigma.mapping.ClassEntry;
35import cuchaz.enigma.mapping.Mappings;
36import cuchaz.enigma.mapping.MappingsChecker;
37import cuchaz.enigma.throwables.MappingConflict;
38import de.sciss.syntaxpane.DefaultSyntaxKit;
39
40
41public class ClassMatchingGui {
42
43 private enum SourceType {
44 Matched {
45 @Override
46 public Collection<ClassEntry> getSourceClasses(ClassMatches matches) {
47 return matches.getUniqueMatches().keySet();
48 }
49 },
50 Unmatched {
51 @Override
52 public Collection<ClassEntry> getSourceClasses(ClassMatches matches) {
53 return matches.getUnmatchedSourceClasses();
54 }
55 },
56 Ambiguous {
57 @Override
58 public Collection<ClassEntry> getSourceClasses(ClassMatches matches) {
59 return matches.getAmbiguouslyMatchedSourceClasses();
60 }
61 };
62
63 public JRadioButton newRadio(ActionListener listener, ButtonGroup group) {
64 JRadioButton button = new JRadioButton(name(), this == getDefault());
65 button.setActionCommand(name());
66 button.addActionListener(listener);
67 group.add(button);
68 return button;
69 }
70
71 public abstract Collection<ClassEntry> getSourceClasses(ClassMatches matches);
72
73 public static SourceType getDefault() {
74 return values()[0];
75 }
76 }
77
78 public interface SaveListener {
79 void save(ClassMatches matches);
80 }
81
82 // controls
83 private JFrame m_frame;
84 private ClassSelector m_sourceClasses;
85 private ClassSelector m_destClasses;
86 private CodeReader m_sourceReader;
87 private CodeReader m_destReader;
88 private JLabel m_sourceClassLabel;
89 private JLabel m_destClassLabel;
90 private JButton m_matchButton;
91 private Map<SourceType, JRadioButton> m_sourceTypeButtons;
92 private JCheckBox m_advanceCheck;
93 private JCheckBox m_top10Matches;
94
95 private ClassMatches m_classMatches;
96 private Deobfuscator m_sourceDeobfuscator;
97 private Deobfuscator m_destDeobfuscator;
98 private ClassEntry m_sourceClass;
99 private ClassEntry m_destClass;
100 private SourceType m_sourceType;
101 private SaveListener m_saveListener;
102
103 public ClassMatchingGui(ClassMatches matches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) {
104
105 m_classMatches = matches;
106 m_sourceDeobfuscator = sourceDeobfuscator;
107 m_destDeobfuscator = destDeobfuscator;
108
109 // init frame
110 m_frame = new JFrame(Constants.NAME + " - Class Matcher");
111 final Container pane = m_frame.getContentPane();
112 pane.setLayout(new BorderLayout());
113
114 // init source side
115 JPanel sourcePanel = new JPanel();
116 sourcePanel.setLayout(new BoxLayout(sourcePanel, BoxLayout.PAGE_AXIS));
117 sourcePanel.setPreferredSize(new Dimension(200, 0));
118 pane.add(sourcePanel, BorderLayout.WEST);
119 sourcePanel.add(new JLabel("Source Classes"));
120
121 // init source type radios
122 JPanel sourceTypePanel = new JPanel();
123 sourcePanel.add(sourceTypePanel);
124 sourceTypePanel.setLayout(new BoxLayout(sourceTypePanel, BoxLayout.PAGE_AXIS));
125 ActionListener sourceTypeListener = event -> setSourceType(SourceType.valueOf(event.getActionCommand()));
126 ButtonGroup sourceTypeButtons = new ButtonGroup();
127 m_sourceTypeButtons = Maps.newHashMap();
128 for (SourceType sourceType : SourceType.values()) {
129 JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons);
130 m_sourceTypeButtons.put(sourceType, button);
131 sourceTypePanel.add(button);
132 }
133
134 m_sourceClasses = new ClassSelector(ClassSelector.DEOBF_CLASS_COMPARATOR);
135 m_sourceClasses.setListener(classEntry -> setSourceClass(classEntry));
136 JScrollPane sourceScroller = new JScrollPane(m_sourceClasses);
137 sourcePanel.add(sourceScroller);
138
139 // init dest side
140 JPanel destPanel = new JPanel();
141 destPanel.setLayout(new BoxLayout(destPanel, BoxLayout.PAGE_AXIS));
142 destPanel.setPreferredSize(new Dimension(200, 0));
143 pane.add(destPanel, BorderLayout.WEST);
144 destPanel.add(new JLabel("Destination Classes"));
145
146 m_top10Matches = new JCheckBox("Show only top 10 matches");
147 destPanel.add(m_top10Matches);
148 m_top10Matches.addActionListener(event -> toggleTop10Matches());
149
150 m_destClasses = new ClassSelector(ClassSelector.DEOBF_CLASS_COMPARATOR);
151 m_destClasses.setListener(this::setDestClass);
152 JScrollPane destScroller = new JScrollPane(m_destClasses);
153 destPanel.add(destScroller);
154
155 JButton autoMatchButton = new JButton("AutoMatch");
156 autoMatchButton.addActionListener(event -> autoMatch());
157 destPanel.add(autoMatchButton);
158
159 // init source panels
160 DefaultSyntaxKit.initKit();
161 m_sourceReader = new CodeReader();
162 m_destReader = new CodeReader();
163
164 // init all the splits
165 JSplitPane splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, sourcePanel, new JScrollPane(m_sourceReader));
166 splitLeft.setResizeWeight(0); // let the right side take all the slack
167 JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(m_destReader), destPanel);
168 splitRight.setResizeWeight(1); // let the left side take all the slack
169 JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, splitLeft, splitRight);
170 splitCenter.setResizeWeight(0.5); // resize 50:50
171 pane.add(splitCenter, BorderLayout.CENTER);
172 splitCenter.resetToPreferredSizes();
173
174 // init bottom panel
175 JPanel bottomPanel = new JPanel();
176 bottomPanel.setLayout(new FlowLayout());
177
178 m_sourceClassLabel = new JLabel();
179 m_sourceClassLabel.setHorizontalAlignment(SwingConstants.RIGHT);
180 m_destClassLabel = new JLabel();
181 m_destClassLabel.setHorizontalAlignment(SwingConstants.LEFT);
182
183 m_matchButton = new JButton();
184
185 m_advanceCheck = new JCheckBox("Advance to next likely match");
186 m_advanceCheck.addActionListener(event -> {
187 if (m_advanceCheck.isSelected()) {
188 advance();
189 }
190 });
191
192 bottomPanel.add(m_sourceClassLabel);
193 bottomPanel.add(m_matchButton);
194 bottomPanel.add(m_destClassLabel);
195 bottomPanel.add(m_advanceCheck);
196 pane.add(bottomPanel, BorderLayout.SOUTH);
197
198 // show the frame
199 pane.doLayout();
200 m_frame.setSize(1024, 576);
201 m_frame.setMinimumSize(new Dimension(640, 480));
202 m_frame.setVisible(true);
203 m_frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
204
205 // init state
206 updateDestMappings();
207 setSourceType(SourceType.getDefault());
208 updateMatchButton();
209 m_saveListener = null;
210 }
211
212 public void setSaveListener(SaveListener val) {
213 m_saveListener = val;
214 }
215
216 private void updateDestMappings() {
217 try {
218 Mappings newMappings = MappingsConverter.newMappings(
219 m_classMatches,
220 m_sourceDeobfuscator.getMappings(),
221 m_sourceDeobfuscator,
222 m_destDeobfuscator
223 );
224
225 // look for dropped mappings
226 MappingsChecker checker = new MappingsChecker(m_destDeobfuscator.getJarIndex());
227 checker.dropBrokenMappings(newMappings);
228
229 // count them
230 int numDroppedFields = checker.getDroppedFieldMappings().size();
231 int numDroppedMethods = checker.getDroppedMethodMappings().size();
232 System.out.println(String.format(
233 "%d mappings from matched classes don't match the dest jar:\n\t%5d fields\n\t%5d methods",
234 numDroppedFields + numDroppedMethods,
235 numDroppedFields,
236 numDroppedMethods
237 ));
238
239 m_destDeobfuscator.setMappings(newMappings);
240 } catch (MappingConflict ex) {
241 System.out.println(ex.getMessage());
242 ex.printStackTrace();
243 return;
244 }
245 }
246
247 protected void setSourceType(SourceType val) {
248
249 // show the source classes
250 m_sourceType = val;
251 m_sourceClasses.setClasses(deobfuscateClasses(m_sourceType.getSourceClasses(m_classMatches), m_sourceDeobfuscator));
252
253 // update counts
254 for (SourceType sourceType : SourceType.values()) {
255 m_sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)",
256 sourceType.name(),
257 sourceType.getSourceClasses(m_classMatches).size()
258 ));
259 }
260 }
261
262 private Collection<ClassEntry> deobfuscateClasses(Collection<ClassEntry> in, Deobfuscator deobfuscator) {
263 List<ClassEntry> out = Lists.newArrayList();
264 for (ClassEntry entry : in) {
265
266 ClassEntry deobf = deobfuscator.deobfuscateEntry(entry);
267
268 // make sure we preserve any scores
269 if (entry instanceof ScoredClassEntry) {
270 deobf = new ScoredClassEntry(deobf, ((ScoredClassEntry) entry).getScore());
271 }
272
273 out.add(deobf);
274 }
275 return out;
276 }
277
278 protected void setSourceClass(ClassEntry classEntry) {
279
280 Runnable onGetDestClasses = null;
281 if (m_advanceCheck.isSelected()) {
282 onGetDestClasses = this::pickBestDestClass;
283 }
284
285 setSourceClass(classEntry, onGetDestClasses);
286 }
287
288 protected void setSourceClass(ClassEntry classEntry, final Runnable onGetDestClasses) {
289
290 // update the current source class
291 m_sourceClass = classEntry;
292 m_sourceClassLabel.setText(m_sourceClass != null ? m_sourceClass.getName() : "");
293
294 if (m_sourceClass != null) {
295
296 // show the dest class(es)
297 ClassMatch match = m_classMatches.getMatchBySource(m_sourceDeobfuscator.obfuscateEntry(m_sourceClass));
298 assert (match != null);
299 if (match.destClasses.isEmpty()) {
300
301 m_destClasses.setClasses(null);
302
303 // run in a separate thread to keep ui responsive
304 new Thread() {
305 @Override
306 public void run() {
307 m_destClasses.setClasses(deobfuscateClasses(getLikelyMatches(m_sourceClass), m_destDeobfuscator));
308 m_destClasses.expandAll();
309
310 if (onGetDestClasses != null) {
311 onGetDestClasses.run();
312 }
313 }
314 }.start();
315
316 } else {
317
318 m_destClasses.setClasses(deobfuscateClasses(match.destClasses, m_destDeobfuscator));
319 m_destClasses.expandAll();
320
321 if (onGetDestClasses != null) {
322 onGetDestClasses.run();
323 }
324 }
325 }
326
327 setDestClass(null);
328 m_sourceReader.decompileClass(m_sourceClass, m_sourceDeobfuscator, () -> m_sourceReader.navigateToClassDeclaration(m_sourceClass));
329
330 updateMatchButton();
331 }
332
333 private Collection<ClassEntry> getLikelyMatches(ClassEntry sourceClass) {
334
335 ClassEntry obfSourceClass = m_sourceDeobfuscator.obfuscateEntry(sourceClass);
336
337 // set up identifiers
338 ClassNamer namer = new ClassNamer(m_classMatches.getUniqueMatches());
339 ClassIdentifier sourceIdentifier = new ClassIdentifier(
340 m_sourceDeobfuscator.getJar(), m_sourceDeobfuscator.getJarIndex(),
341 namer.getSourceNamer(), true
342 );
343 ClassIdentifier destIdentifier = new ClassIdentifier(
344 m_destDeobfuscator.getJar(), m_destDeobfuscator.getJarIndex(),
345 namer.getDestNamer(), true
346 );
347
348 try {
349
350 // rank all the unmatched dest classes against the source class
351 ClassIdentity sourceIdentity = sourceIdentifier.identify(obfSourceClass);
352 List<ClassEntry> scoredDestClasses = Lists.newArrayList();
353 for (ClassEntry unmatchedDestClass : m_classMatches.getUnmatchedDestClasses()) {
354 ClassIdentity destIdentity = destIdentifier.identify(unmatchedDestClass);
355 float score = 100.0f * (sourceIdentity.getMatchScore(destIdentity) + destIdentity.getMatchScore(sourceIdentity))
356 / (sourceIdentity.getMaxMatchScore() + destIdentity.getMaxMatchScore());
357 scoredDestClasses.add(new ScoredClassEntry(unmatchedDestClass, score));
358 }
359
360 if (m_top10Matches.isSelected() && scoredDestClasses.size() > 10) {
361 Collections.sort(scoredDestClasses, (a, b) -> {
362 ScoredClassEntry sa = (ScoredClassEntry) a;
363 ScoredClassEntry sb = (ScoredClassEntry) b;
364 return -Float.compare(sa.getScore(), sb.getScore());
365 });
366 scoredDestClasses = scoredDestClasses.subList(0, 10);
367 }
368
369 return scoredDestClasses;
370
371 } catch (ClassNotFoundException ex) {
372 throw new Error("Unable to find class " + ex.getMessage());
373 }
374 }
375
376 protected void setDestClass(ClassEntry classEntry) {
377
378 // update the current source class
379 m_destClass = classEntry;
380 m_destClassLabel.setText(m_destClass != null ? m_destClass.getName() : "");
381
382 m_destReader.decompileClass(m_destClass, m_destDeobfuscator, () -> m_destReader.navigateToClassDeclaration(m_destClass));
383
384 updateMatchButton();
385 }
386
387 private void updateMatchButton() {
388
389 ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass);
390 ClassEntry obfDest = m_destDeobfuscator.obfuscateEntry(m_destClass);
391
392 BiMap<ClassEntry, ClassEntry> uniqueMatches = m_classMatches.getUniqueMatches();
393 boolean twoSelected = m_sourceClass != null && m_destClass != null;
394 boolean isMatched = uniqueMatches.containsKey(obfSource) && uniqueMatches.containsValue(obfDest);
395 boolean canMatch = !uniqueMatches.containsKey(obfSource) && !uniqueMatches.containsValue(obfDest);
396
397 GuiTricks.deactivateButton(m_matchButton);
398 if (twoSelected) {
399 if (isMatched) {
400 GuiTricks.activateButton(m_matchButton, "Unmatch", event -> onUnmatchClick());
401 } else if (canMatch) {
402 GuiTricks.activateButton(m_matchButton, "Match", event -> onMatchClick());
403 }
404 }
405 }
406
407 private void onMatchClick() {
408 // precondition: source and dest classes are set correctly
409
410 ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass);
411 ClassEntry obfDest = m_destDeobfuscator.obfuscateEntry(m_destClass);
412
413 // remove the classes from their match
414 m_classMatches.removeSource(obfSource);
415 m_classMatches.removeDest(obfDest);
416
417 // add them as matched classes
418 m_classMatches.add(new ClassMatch(obfSource, obfDest));
419
420 ClassEntry nextClass = null;
421 if (m_advanceCheck.isSelected()) {
422 nextClass = m_sourceClasses.getNextClass(m_sourceClass);
423 }
424
425 save();
426 updateMatches();
427
428 if (nextClass != null) {
429 advance(nextClass);
430 }
431 }
432
433 private void onUnmatchClick() {
434 // precondition: source and dest classes are set to a unique match
435
436 ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass);
437
438 // remove the source to break the match, then add the source back as unmatched
439 m_classMatches.removeSource(obfSource);
440 m_classMatches.add(new ClassMatch(obfSource, null));
441
442 save();
443 updateMatches();
444 }
445
446 private void updateMatches() {
447 updateDestMappings();
448 setDestClass(null);
449 m_destClasses.setClasses(null);
450 updateMatchButton();
451
452 // remember where we were in the source tree
453 String packageName = m_sourceClasses.getSelectedPackage();
454
455 setSourceType(m_sourceType);
456
457 m_sourceClasses.expandPackage(packageName);
458 }
459
460 private void save() {
461 if (m_saveListener != null) {
462 m_saveListener.save(m_classMatches);
463 }
464 }
465
466 private void autoMatch() {
467
468 System.out.println("Automatching...");
469
470 // compute a new matching
471 ClassMatching matching = MappingsConverter.computeMatching(
472 m_sourceDeobfuscator.getJar(), m_sourceDeobfuscator.getJarIndex(),
473 m_destDeobfuscator.getJar(), m_destDeobfuscator.getJarIndex(),
474 m_classMatches.getUniqueMatches()
475 );
476 ClassMatches newMatches = new ClassMatches(matching.matches());
477 System.out.println(String.format("Automatch found %d new matches",
478 newMatches.getUniqueMatches().size() - m_classMatches.getUniqueMatches().size()
479 ));
480
481 // update the current matches
482 m_classMatches = newMatches;
483 save();
484 updateMatches();
485 }
486
487 private void advance() {
488 advance(null);
489 }
490
491 private void advance(ClassEntry sourceClass) {
492
493 // make sure we have a source class
494 if (sourceClass == null) {
495 sourceClass = m_sourceClasses.getSelectedClass();
496 if (sourceClass != null) {
497 sourceClass = m_sourceClasses.getNextClass(sourceClass);
498 } else {
499 sourceClass = m_sourceClasses.getFirstClass();
500 }
501 }
502
503 // set the source class
504 setSourceClass(sourceClass, this::pickBestDestClass);
505 m_sourceClasses.setSelectionClass(sourceClass);
506 }
507
508 private void pickBestDestClass() {
509
510 // then, pick the best dest class
511 ClassEntry firstClass = null;
512 ScoredClassEntry bestDestClass = null;
513 for (ClassSelectorPackageNode packageNode : m_destClasses.packageNodes()) {
514 for (ClassSelectorClassNode classNode : m_destClasses.classNodes(packageNode)) {
515 if (firstClass == null) {
516 firstClass = classNode.getClassEntry();
517 }
518 if (classNode.getClassEntry() instanceof ScoredClassEntry) {
519 ScoredClassEntry scoredClass = (ScoredClassEntry) classNode.getClassEntry();
520 if (bestDestClass == null || bestDestClass.getScore() < scoredClass.getScore()) {
521 bestDestClass = scoredClass;
522 }
523 }
524 }
525 }
526
527 // pick the entry to show
528 ClassEntry destClass = null;
529 if (bestDestClass != null) {
530 destClass = bestDestClass;
531 } else if (firstClass != null) {
532 destClass = firstClass;
533 }
534
535 setDestClass(destClass);
536 m_destClasses.setSelectionClass(destClass);
537 }
538
539 private void toggleTop10Matches() {
540 if (m_sourceClass != null) {
541 m_destClasses.clearSelection();
542 m_destClasses.setClasses(deobfuscateClasses(getLikelyMatches(m_sourceClass), m_destDeobfuscator));
543 m_destClasses.expandAll();
544 }
545 }
546}