/*******************************************************************************
* Copyright (c) 2015 Jeff Martin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public
* License v3.0 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* Contributors:
* Jeff Martin - initial API and implementation
******************************************************************************/
package cuchaz.enigma.gui;
import com.google.common.collect.BiMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.swing.*;
import cuchaz.enigma.Constants;
import cuchaz.enigma.Deobfuscator;
import cuchaz.enigma.convert.*;
import cuchaz.enigma.mapping.ClassEntry;
import cuchaz.enigma.mapping.Mappings;
import cuchaz.enigma.mapping.MappingsChecker;
import de.sciss.syntaxpane.DefaultSyntaxKit;
public class ClassMatchingGui {
private enum SourceType {
Matched {
@Override
public Collection getSourceClasses(ClassMatches matches) {
return matches.getUniqueMatches().keySet();
}
},
Unmatched {
@Override
public Collection getSourceClasses(ClassMatches matches) {
return matches.getUnmatchedSourceClasses();
}
},
Ambiguous {
@Override
public Collection getSourceClasses(ClassMatches matches) {
return matches.getAmbiguouslyMatchedSourceClasses();
}
};
public JRadioButton newRadio(ActionListener listener, ButtonGroup group) {
JRadioButton button = new JRadioButton(name(), this == getDefault());
button.setActionCommand(name());
button.addActionListener(listener);
group.add(button);
return button;
}
public abstract Collection getSourceClasses(ClassMatches matches);
public static SourceType getDefault() {
return values()[0];
}
}
public interface SaveListener {
void save(ClassMatches matches);
}
// controls
private JFrame frame;
private ClassSelector sourceClasses;
private ClassSelector destClasses;
private CodeReader sourceReader;
private CodeReader destReader;
private JLabel sourceClassLabel;
private JLabel destClassLabel;
private JButton matchButton;
private Map sourceTypeButtons;
private JCheckBox advanceCheck;
private JCheckBox top10Matches;
private ClassMatches classMatches;
private Deobfuscator sourceDeobfuscator;
private Deobfuscator destDeobfuscator;
private ClassEntry sourceClass;
private ClassEntry destClass;
private SourceType sourceType;
private SaveListener saveListener;
public ClassMatchingGui(ClassMatches matches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) {
this.classMatches = matches;
this.sourceDeobfuscator = sourceDeobfuscator;
this.destDeobfuscator = destDeobfuscator;
// init frame
this.frame = new JFrame(Constants.NAME + " - Class Matcher");
final Container pane = this.frame.getContentPane();
pane.setLayout(new BorderLayout());
// init source side
JPanel sourcePanel = new JPanel();
sourcePanel.setLayout(new BoxLayout(sourcePanel, BoxLayout.PAGE_AXIS));
sourcePanel.setPreferredSize(new Dimension(200, 0));
pane.add(sourcePanel, BorderLayout.WEST);
sourcePanel.add(new JLabel("Source Classes"));
// init source type radios
JPanel sourceTypePanel = new JPanel();
sourcePanel.add(sourceTypePanel);
sourceTypePanel.setLayout(new BoxLayout(sourceTypePanel, BoxLayout.PAGE_AXIS));
ActionListener sourceTypeListener = event -> setSourceType(SourceType.valueOf(event.getActionCommand()));
ButtonGroup sourceTypeButtons = new ButtonGroup();
this.sourceTypeButtons = Maps.newHashMap();
for (SourceType sourceType : SourceType.values()) {
JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons);
this.sourceTypeButtons.put(sourceType, button);
sourceTypePanel.add(button);
}
this.sourceClasses = new ClassSelector(ClassSelector.DEOBF_CLASS_COMPARATOR);
this.sourceClasses.setListener(this::setSourceClass);
JScrollPane sourceScroller = new JScrollPane(this.sourceClasses);
sourcePanel.add(sourceScroller);
// init dest side
JPanel destPanel = new JPanel();
destPanel.setLayout(new BoxLayout(destPanel, BoxLayout.PAGE_AXIS));
destPanel.setPreferredSize(new Dimension(200, 0));
pane.add(destPanel, BorderLayout.WEST);
destPanel.add(new JLabel("Destination Classes"));
this.top10Matches = new JCheckBox("Show only top 10 matches");
destPanel.add(this.top10Matches);
this.top10Matches.addActionListener(event -> toggleTop10Matches());
this.destClasses = new ClassSelector(ClassSelector.DEOBF_CLASS_COMPARATOR);
this.destClasses.setListener(this::setDestClass);
JScrollPane destScroller = new JScrollPane(this.destClasses);
destPanel.add(destScroller);
JButton autoMatchButton = new JButton("AutoMatch");
autoMatchButton.addActionListener(event -> autoMatch());
destPanel.add(autoMatchButton);
// init source panels
DefaultSyntaxKit.initKit();
this.sourceReader = new CodeReader();
this.destReader = new CodeReader();
// init all the splits
JSplitPane splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, sourcePanel, new JScrollPane(this.sourceReader));
splitLeft.setResizeWeight(0); // let the right side take all the slack
JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(this.destReader), destPanel);
splitRight.setResizeWeight(1); // let the left side take all the slack
JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, splitLeft, splitRight);
splitCenter.setResizeWeight(0.5); // resize 50:50
pane.add(splitCenter, BorderLayout.CENTER);
splitCenter.resetToPreferredSizes();
// init bottom panel
JPanel bottomPanel = new JPanel();
bottomPanel.setLayout(new FlowLayout());
this.sourceClassLabel = new JLabel();
this.sourceClassLabel.setHorizontalAlignment(SwingConstants.RIGHT);
this.destClassLabel = new JLabel();
this.destClassLabel.setHorizontalAlignment(SwingConstants.LEFT);
this.matchButton = new JButton();
this.advanceCheck = new JCheckBox("Advance to next likely match");
this.advanceCheck.addActionListener(event -> {
if (this.advanceCheck.isSelected()) {
advance();
}
});
bottomPanel.add(this.sourceClassLabel);
bottomPanel.add(this.matchButton);
bottomPanel.add(this.destClassLabel);
bottomPanel.add(this.advanceCheck);
pane.add(bottomPanel, BorderLayout.SOUTH);
// show the frame
pane.doLayout();
this.frame.setSize(1024, 576);
this.frame.setMinimumSize(new Dimension(640, 480));
this.frame.setVisible(true);
this.frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// init state
updateDestMappings();
setSourceType(SourceType.getDefault());
updateMatchButton();
this.saveListener = null;
}
public void setSaveListener(SaveListener val) {
this.saveListener = val;
}
private void updateDestMappings() {
Mappings newMappings = MappingsConverter.newMappings(this.classMatches, this.sourceDeobfuscator.getMappings(), this.sourceDeobfuscator, this.destDeobfuscator);
// look for dropped mappings
MappingsChecker checker = new MappingsChecker(this.destDeobfuscator.getJarIndex());
checker.dropBrokenMappings(newMappings);
// count them
int numDroppedFields = checker.getDroppedFieldMappings().size();
int numDroppedMethods = checker.getDroppedMethodMappings().size();
System.out.println(String.format("%d mappings from matched classes don't match the dest jar:\n\t%5d fields\n\t%5d methods",
numDroppedFields + numDroppedMethods,
numDroppedFields,
numDroppedMethods
));
this.destDeobfuscator.setMappings(newMappings);
}
protected void setSourceType(SourceType val) {
// show the source classes
this.sourceType = val;
this.sourceClasses.setClasses(deobfuscateClasses(this.sourceType.getSourceClasses(this.classMatches), this.sourceDeobfuscator));
// update counts
for (SourceType sourceType : SourceType.values()) {
this.sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)",
sourceType.name(),
sourceType.getSourceClasses(this.classMatches).size()
));
}
}
private Collection deobfuscateClasses(Collection in, Deobfuscator deobfuscator) {
List out = Lists.newArrayList();
for (ClassEntry entry : in) {
ClassEntry deobf = deobfuscator.deobfuscateEntry(entry);
// make sure we preserve any scores
if (entry instanceof ScoredClassEntry) {
deobf = new ScoredClassEntry(deobf, ((ScoredClassEntry) entry).getScore());
}
out.add(deobf);
}
return out;
}
protected void setSourceClass(ClassEntry classEntry) {
Runnable onGetDestClasses = null;
if (this.advanceCheck.isSelected()) {
onGetDestClasses = this::pickBestDestClass;
}
setSourceClass(classEntry, onGetDestClasses);
}
protected void setSourceClass(ClassEntry classEntry, final Runnable onGetDestClasses) {
// update the current source class
this.sourceClass = classEntry;
this.sourceClassLabel.setText(this.sourceClass != null ? this.sourceClass.getName() : "");
if (this.sourceClass != null) {
// show the dest class(es)
ClassMatch match = this.classMatches.getMatchBySource(this.sourceDeobfuscator.obfuscateEntry(this.sourceClass));
assert (match != null);
if (match.destClasses.isEmpty()) {
this.destClasses.setClasses(null);
// run in a separate thread to keep ui responsive
new Thread() {
@Override
public void run() {
destClasses.setClasses(deobfuscateClasses(getLikelyMatches(sourceClass), destDeobfuscator));
destClasses.expandAll();
if (onGetDestClasses != null) {
onGetDestClasses.run();
}
}
}.start();
} else {
this.destClasses.setClasses(deobfuscateClasses(match.destClasses, this.destDeobfuscator));
this.destClasses.expandAll();
if (onGetDestClasses != null) {
onGetDestClasses.run();
}
}
}
setDestClass(null);
this.sourceReader.decompileClass(this.sourceClass, this.sourceDeobfuscator, () -> this.sourceReader.navigateToClassDeclaration(this.sourceClass));
updateMatchButton();
}
private Collection getLikelyMatches(ClassEntry sourceClass) {
ClassEntry obfSourceClass = this.sourceDeobfuscator.obfuscateEntry(sourceClass);
// set up identifiers
ClassNamer namer = new ClassNamer(this.classMatches.getUniqueMatches());
ClassIdentifier sourceIdentifier = new ClassIdentifier(this.sourceDeobfuscator.getJar(), this.sourceDeobfuscator.getJarIndex(), namer.getSourceNamer(), true);
ClassIdentifier destIdentifier = new ClassIdentifier(this.destDeobfuscator.getJar(), this.destDeobfuscator.getJarIndex(), namer.getDestNamer(), true);
try {
// rank all the unmatched dest classes against the source class
ClassIdentity sourceIdentity = sourceIdentifier.identify(obfSourceClass);
List scoredDestClasses = Lists.newArrayList();
for (ClassEntry unmatchedDestClass : this.classMatches.getUnmatchedDestClasses()) {
ClassIdentity destIdentity = destIdentifier.identify(unmatchedDestClass);
float score = 100.0f * (sourceIdentity.getMatchScore(destIdentity) + destIdentity.getMatchScore(sourceIdentity))
/ (sourceIdentity.getMaxMatchScore() + destIdentity.getMaxMatchScore());
scoredDestClasses.add(new ScoredClassEntry(unmatchedDestClass, score));
}
if (this.top10Matches.isSelected() && scoredDestClasses.size() > 10) {
Collections.sort(scoredDestClasses, (a, b) -> {
ScoredClassEntry sa = (ScoredClassEntry) a;
ScoredClassEntry sb = (ScoredClassEntry) b;
return -Float.compare(sa.getScore(), sb.getScore());
});
scoredDestClasses = scoredDestClasses.subList(0, 10);
}
return scoredDestClasses;
} catch (ClassNotFoundException ex) {
throw new Error("Unable to find class " + ex.getMessage());
}
}
protected void setDestClass(ClassEntry classEntry) {
// update the current source class
this.destClass = classEntry;
this.destClassLabel.setText(this.destClass != null ? this.destClass.getName() : "");
this.destReader.decompileClass(this.destClass, this.destDeobfuscator, () -> this.destReader.navigateToClassDeclaration(this.destClass));
updateMatchButton();
}
private void updateMatchButton() {
ClassEntry obfSource = this.sourceDeobfuscator.obfuscateEntry(this.sourceClass);
ClassEntry obfDest = this.destDeobfuscator.obfuscateEntry(this.destClass);
BiMap uniqueMatches = this.classMatches.getUniqueMatches();
boolean twoSelected = this.sourceClass != null && this.destClass != null;
boolean isMatched = uniqueMatches.containsKey(obfSource) && uniqueMatches.containsValue(obfDest);
boolean canMatch = !uniqueMatches.containsKey(obfSource) && !uniqueMatches.containsValue(obfDest);
GuiTricks.deactivateButton(this.matchButton);
if (twoSelected) {
if (isMatched) {
GuiTricks.activateButton(this.matchButton, "Unmatch", event -> onUnmatchClick());
} else if (canMatch) {
GuiTricks.activateButton(this.matchButton, "Match", event -> onMatchClick());
}
}
}
private void onMatchClick() {
// precondition: source and dest classes are set correctly
ClassEntry obfSource = this.sourceDeobfuscator.obfuscateEntry(this.sourceClass);
ClassEntry obfDest = this.destDeobfuscator.obfuscateEntry(this.destClass);
// remove the classes from their match
this.classMatches.removeSource(obfSource);
this.classMatches.removeDest(obfDest);
// add them as matched classes
this.classMatches.add(new ClassMatch(obfSource, obfDest));
ClassEntry nextClass = null;
if (this.advanceCheck.isSelected()) {
nextClass = this.sourceClasses.getNextClass(this.sourceClass);
}
save();
updateMatches();
if (nextClass != null) {
advance(nextClass);
}
}
private void onUnmatchClick() {
// precondition: source and dest classes are set to a unique match
ClassEntry obfSource = this.sourceDeobfuscator.obfuscateEntry(this.sourceClass);
// remove the source to break the match, then add the source back as unmatched
this.classMatches.removeSource(obfSource);
this.classMatches.add(new ClassMatch(obfSource, null));
save();
updateMatches();
}
private void updateMatches() {
updateDestMappings();
setDestClass(null);
this.destClasses.setClasses(null);
updateMatchButton();
// remember where we were in the source tree
String packageName = this.sourceClasses.getSelectedPackage();
setSourceType(this.sourceType);
this.sourceClasses.expandPackage(packageName);
}
private void save() {
if (this.saveListener != null) {
this.saveListener.save(this.classMatches);
}
}
private void autoMatch() {
System.out.println("Automatching...");
// compute a new matching
ClassMatching matching = MappingsConverter.computeMatching(this.sourceDeobfuscator.getJar(), this.sourceDeobfuscator.getJarIndex(),
this.destDeobfuscator.getJar(), this.destDeobfuscator.getJarIndex(), this.classMatches.getUniqueMatches());
ClassMatches newMatches = new ClassMatches(matching.matches());
System.out.println(String.format("Automatch found %d new matches", newMatches.getUniqueMatches().size() - this.classMatches.getUniqueMatches().size()));
// update the current matches
this.classMatches = newMatches;
save();
updateMatches();
}
private void advance() {
advance(null);
}
private void advance(ClassEntry sourceClass) {
// make sure we have a source class
if (sourceClass == null) {
sourceClass = this.sourceClasses.getSelectedClass();
if (sourceClass != null) {
sourceClass = this.sourceClasses.getNextClass(sourceClass);
} else {
sourceClass = this.sourceClasses.getFirstClass();
}
}
// set the source class
setSourceClass(sourceClass, this::pickBestDestClass);
this.sourceClasses.setSelectionClass(sourceClass);
}
private void pickBestDestClass() {
// then, pick the best dest class
ClassEntry firstClass = null;
ScoredClassEntry bestDestClass = null;
for (ClassSelectorPackageNode packageNode : this.destClasses.packageNodes()) {
for (ClassSelectorClassNode classNode : this.destClasses.classNodes(packageNode)) {
if (firstClass == null) {
firstClass = classNode.getClassEntry();
}
if (classNode.getClassEntry() instanceof ScoredClassEntry) {
ScoredClassEntry scoredClass = (ScoredClassEntry) classNode.getClassEntry();
if (bestDestClass == null || bestDestClass.getScore() < scoredClass.getScore()) {
bestDestClass = scoredClass;
}
}
}
}
// pick the entry to show
ClassEntry destClass = null;
if (bestDestClass != null) {
destClass = bestDestClass;
} else if (firstClass != null) {
destClass = firstClass;
}
setDestClass(destClass);
this.destClasses.setSelectionClass(destClass);
}
private void toggleTop10Matches() {
if (this.sourceClass != null) {
this.destClasses.clearSelection();
this.destClasses.setClasses(deobfuscateClasses(getLikelyMatches(this.sourceClass), this.destDeobfuscator));
this.destClasses.expandAll();
}
}
}