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