From e3f452250e51b7271f3989c7dfd12e4422934942 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 21 May 2015 23:30:00 +0100 Subject: Support Gradle alongside SSJB This makes builds faster, simpler and better automated but still keeps Cuchaz happy. :) --- src/cuchaz/enigma/gui/AboutDialog.java | 86 ++ src/cuchaz/enigma/gui/BoxHighlightPainter.java | 64 ++ src/cuchaz/enigma/gui/BrowserCaret.java | 45 + src/cuchaz/enigma/gui/ClassListCellRenderer.java | 36 + src/cuchaz/enigma/gui/ClassMatchingGui.java | 589 ++++++++++ src/cuchaz/enigma/gui/ClassSelector.java | 293 +++++ src/cuchaz/enigma/gui/ClassSelectorClassNode.java | 50 + .../enigma/gui/ClassSelectorPackageNode.java | 45 + src/cuchaz/enigma/gui/CodeReader.java | 222 ++++ src/cuchaz/enigma/gui/CrashDialog.java | 101 ++ .../enigma/gui/DeobfuscatedHighlightPainter.java | 21 + src/cuchaz/enigma/gui/Gui.java | 1122 ++++++++++++++++++++ src/cuchaz/enigma/gui/GuiController.java | 358 +++++++ src/cuchaz/enigma/gui/GuiTricks.java | 56 + src/cuchaz/enigma/gui/MemberMatchingGui.java | 499 +++++++++ .../enigma/gui/ObfuscatedHighlightPainter.java | 21 + src/cuchaz/enigma/gui/OtherHighlightPainter.java | 21 + src/cuchaz/enigma/gui/ProgressDialog.java | 105 ++ src/cuchaz/enigma/gui/ReadableToken.java | 36 + src/cuchaz/enigma/gui/RenameListener.java | 17 + src/cuchaz/enigma/gui/ScoredClassEntry.java | 30 + .../enigma/gui/SelectionHighlightPainter.java | 34 + src/cuchaz/enigma/gui/TokenListCellRenderer.java | 38 + 23 files changed, 3889 insertions(+) create mode 100644 src/cuchaz/enigma/gui/AboutDialog.java create mode 100644 src/cuchaz/enigma/gui/BoxHighlightPainter.java create mode 100644 src/cuchaz/enigma/gui/BrowserCaret.java create mode 100644 src/cuchaz/enigma/gui/ClassListCellRenderer.java create mode 100644 src/cuchaz/enigma/gui/ClassMatchingGui.java create mode 100644 src/cuchaz/enigma/gui/ClassSelector.java create mode 100644 src/cuchaz/enigma/gui/ClassSelectorClassNode.java create mode 100644 src/cuchaz/enigma/gui/ClassSelectorPackageNode.java create mode 100644 src/cuchaz/enigma/gui/CodeReader.java create mode 100644 src/cuchaz/enigma/gui/CrashDialog.java create mode 100644 src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java create mode 100644 src/cuchaz/enigma/gui/Gui.java create mode 100644 src/cuchaz/enigma/gui/GuiController.java create mode 100644 src/cuchaz/enigma/gui/GuiTricks.java create mode 100644 src/cuchaz/enigma/gui/MemberMatchingGui.java create mode 100644 src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java create mode 100644 src/cuchaz/enigma/gui/OtherHighlightPainter.java create mode 100644 src/cuchaz/enigma/gui/ProgressDialog.java create mode 100644 src/cuchaz/enigma/gui/ReadableToken.java create mode 100644 src/cuchaz/enigma/gui/RenameListener.java create mode 100644 src/cuchaz/enigma/gui/ScoredClassEntry.java create mode 100644 src/cuchaz/enigma/gui/SelectionHighlightPainter.java create mode 100644 src/cuchaz/enigma/gui/TokenListCellRenderer.java (limited to 'src/cuchaz/enigma/gui') diff --git a/src/cuchaz/enigma/gui/AboutDialog.java b/src/cuchaz/enigma/gui/AboutDialog.java new file mode 100644 index 0000000..3eba1e5 --- /dev/null +++ b/src/cuchaz/enigma/gui/AboutDialog.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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 java.awt.Color; +import java.awt.Container; +import java.awt.Cursor; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.WindowConstants; + +import cuchaz.enigma.Constants; +import cuchaz.enigma.Util; + +public class AboutDialog { + + public static void show(JFrame parent) { + // init frame + final JFrame frame = new JFrame(Constants.Name + " - About"); + final Container pane = frame.getContentPane(); + pane.setLayout(new FlowLayout()); + + // load the content + try { + String html = Util.readResourceToString("/about.html"); + html = String.format(html, Constants.Name, Constants.Version); + JLabel label = new JLabel(html); + label.setHorizontalAlignment(JLabel.CENTER); + pane.add(label); + } catch (IOException ex) { + throw new Error(ex); + } + + // show the link + String html = "%s"; + html = String.format(html, Constants.Url, Constants.Url); + JButton link = new JButton(html); + link.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + Util.openUrl(Constants.Url); + } + }); + link.setBorderPainted(false); + link.setOpaque(false); + link.setBackground(Color.WHITE); + link.setCursor(new Cursor(Cursor.HAND_CURSOR)); + link.setFocusable(false); + JPanel linkPanel = new JPanel(); + linkPanel.add(link); + pane.add(linkPanel); + + // show ok button + JButton okButton = new JButton("Ok"); + pane.add(okButton); + okButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + frame.dispose(); + } + }); + + // show the frame + pane.doLayout(); + frame.setSize(400, 220); + frame.setResizable(false); + frame.setLocationRelativeTo(parent); + frame.setVisible(true); + frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + } +} diff --git a/src/cuchaz/enigma/gui/BoxHighlightPainter.java b/src/cuchaz/enigma/gui/BoxHighlightPainter.java new file mode 100644 index 0000000..e5e0557 --- /dev/null +++ b/src/cuchaz/enigma/gui/BoxHighlightPainter.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * 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 java.awt.Color; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Shape; + +import javax.swing.text.BadLocationException; +import javax.swing.text.Highlighter; +import javax.swing.text.JTextComponent; + +public abstract class BoxHighlightPainter implements Highlighter.HighlightPainter { + + private Color m_fillColor; + private Color m_borderColor; + + protected BoxHighlightPainter(Color fillColor, Color borderColor) { + m_fillColor = fillColor; + m_borderColor = borderColor; + } + + @Override + public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) { + Rectangle bounds = getBounds(text, start, end); + + // fill the area + if (m_fillColor != null) { + g.setColor(m_fillColor); + g.fillRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); + } + + // draw a box around the area + g.setColor(m_borderColor); + g.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); + } + + protected static Rectangle getBounds(JTextComponent text, int start, int end) { + try { + // determine the bounds of the text + Rectangle bounds = text.modelToView(start).union(text.modelToView(end)); + + // adjust the box so it looks nice + bounds.x -= 2; + bounds.width += 2; + bounds.y += 1; + bounds.height -= 2; + + return bounds; + } catch (BadLocationException ex) { + // don't care... just return something + return new Rectangle(0, 0, 0, 0); + } + } +} diff --git a/src/cuchaz/enigma/gui/BrowserCaret.java b/src/cuchaz/enigma/gui/BrowserCaret.java new file mode 100644 index 0000000..6af4d24 --- /dev/null +++ b/src/cuchaz/enigma/gui/BrowserCaret.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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 java.awt.Graphics; +import java.awt.Shape; + +import javax.swing.text.DefaultCaret; +import javax.swing.text.Highlighter; +import javax.swing.text.JTextComponent; + +public class BrowserCaret extends DefaultCaret { + + private static final long serialVersionUID = 1158977422507969940L; + + private static final Highlighter.HighlightPainter m_selectionPainter = new Highlighter.HighlightPainter() { + @Override + public void paint(Graphics g, int p0, int p1, Shape bounds, JTextComponent c) { + // don't paint anything + } + }; + + @Override + public boolean isSelectionVisible() { + return false; + } + + @Override + public boolean isVisible() { + return true; + } + + @Override + public Highlighter.HighlightPainter getSelectionPainter() { + return m_selectionPainter; + } +} diff --git a/src/cuchaz/enigma/gui/ClassListCellRenderer.java b/src/cuchaz/enigma/gui/ClassListCellRenderer.java new file mode 100644 index 0000000..cde3e4c --- /dev/null +++ b/src/cuchaz/enigma/gui/ClassListCellRenderer.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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 java.awt.Component; + +import javassist.bytecode.Descriptor; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; + +public class ClassListCellRenderer implements ListCellRenderer { + + private DefaultListCellRenderer m_defaultRenderer; + + public ClassListCellRenderer() { + m_defaultRenderer = new DefaultListCellRenderer(); + } + + @Override + public Component getListCellRendererComponent(JList list, String className, int index, boolean isSelected, boolean hasFocus) { + JLabel label = (JLabel)m_defaultRenderer.getListCellRendererComponent(list, className, index, isSelected, hasFocus); + label.setText(Descriptor.toJavaName(className)); + return label; + } +} 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 @@ +/******************************************************************************* + * 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 java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.SwingConstants; +import javax.swing.WindowConstants; + +import com.google.common.collect.BiMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import cuchaz.enigma.Constants; +import cuchaz.enigma.Deobfuscator; +import cuchaz.enigma.convert.ClassIdentifier; +import cuchaz.enigma.convert.ClassIdentity; +import cuchaz.enigma.convert.ClassMatch; +import cuchaz.enigma.convert.ClassMatches; +import cuchaz.enigma.convert.ClassMatching; +import cuchaz.enigma.convert.ClassNamer; +import cuchaz.enigma.convert.MappingsConverter; +import cuchaz.enigma.gui.ClassSelector.ClassSelectionListener; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.Mappings; +import cuchaz.enigma.mapping.MappingsChecker; +import de.sciss.syntaxpane.DefaultSyntaxKit; + + +public class ClassMatchingGui { + + private static 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 static interface SaveListener { + public void save(ClassMatches matches); + } + + // controls + private JFrame m_frame; + private ClassSelector m_sourceClasses; + private ClassSelector m_destClasses; + private CodeReader m_sourceReader; + private CodeReader m_destReader; + private JLabel m_sourceClassLabel; + private JLabel m_destClassLabel; + private JButton m_matchButton; + private Map m_sourceTypeButtons; + private JCheckBox m_advanceCheck; + + private ClassMatches m_classMatches; + private Deobfuscator m_sourceDeobfuscator; + private Deobfuscator m_destDeobfuscator; + private ClassEntry m_sourceClass; + private ClassEntry m_destClass; + private SourceType m_sourceType; + private SaveListener m_saveListener; + + public ClassMatchingGui(ClassMatches matches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) { + + m_classMatches = matches; + m_sourceDeobfuscator = sourceDeobfuscator; + m_destDeobfuscator = destDeobfuscator; + + // init frame + m_frame = new JFrame(Constants.Name + " - Class Matcher"); + final Container pane = m_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 = new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + setSourceType(SourceType.valueOf(event.getActionCommand())); + } + }; + ButtonGroup sourceTypeButtons = new ButtonGroup(); + m_sourceTypeButtons = Maps.newHashMap(); + for (SourceType sourceType : SourceType.values()) { + JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons); + m_sourceTypeButtons.put(sourceType, button); + sourceTypePanel.add(button); + } + + m_sourceClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator); + m_sourceClasses.setListener(new ClassSelectionListener() { + @Override + public void onSelectClass(ClassEntry classEntry) { + setSourceClass(classEntry); + } + }); + JScrollPane sourceScroller = new JScrollPane(m_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")); + + m_destClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator); + m_destClasses.setListener(new ClassSelectionListener() { + @Override + public void onSelectClass(ClassEntry classEntry) { + setDestClass(classEntry); + } + }); + JScrollPane destScroller = new JScrollPane(m_destClasses); + destPanel.add(destScroller); + + JButton autoMatchButton = new JButton("AutoMatch"); + autoMatchButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + autoMatch(); + } + }); + destPanel.add(autoMatchButton); + + // init source panels + DefaultSyntaxKit.initKit(); + m_sourceReader = new CodeReader(); + m_destReader = new CodeReader(); + + // init all the splits + JSplitPane splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, sourcePanel, new JScrollPane(m_sourceReader)); + splitLeft.setResizeWeight(0); // let the right side take all the slack + JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(m_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()); + + m_sourceClassLabel = new JLabel(); + m_sourceClassLabel.setHorizontalAlignment(SwingConstants.RIGHT); + m_destClassLabel = new JLabel(); + m_destClassLabel.setHorizontalAlignment(SwingConstants.LEFT); + + m_matchButton = new JButton(); + + m_advanceCheck = new JCheckBox("Advance to next likely match"); + m_advanceCheck.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + if (m_advanceCheck.isSelected()) { + advance(); + } + } + }); + + bottomPanel.add(m_sourceClassLabel); + bottomPanel.add(m_matchButton); + bottomPanel.add(m_destClassLabel); + bottomPanel.add(m_advanceCheck); + pane.add(bottomPanel, BorderLayout.SOUTH); + + // show the frame + pane.doLayout(); + m_frame.setSize(1024, 576); + m_frame.setMinimumSize(new Dimension(640, 480)); + m_frame.setVisible(true); + m_frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + + // init state + updateDestMappings(); + setSourceType(SourceType.getDefault()); + updateMatchButton(); + m_saveListener = null; + } + + public void setSaveListener(SaveListener val) { + m_saveListener = val; + } + + private void updateDestMappings() { + + Mappings newMappings = MappingsConverter.newMappings( + m_classMatches, + m_sourceDeobfuscator.getMappings(), + m_sourceDeobfuscator, + m_destDeobfuscator + ); + + // look for dropped mappings + MappingsChecker checker = new MappingsChecker(m_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 + )); + + m_destDeobfuscator.setMappings(newMappings); + } + + protected void setSourceType(SourceType val) { + + // show the source classes + m_sourceType = val; + m_sourceClasses.setClasses(deobfuscateClasses(m_sourceType.getSourceClasses(m_classMatches), m_sourceDeobfuscator)); + + // update counts + for (SourceType sourceType : SourceType.values()) { + m_sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)", + sourceType.name(), + sourceType.getSourceClasses(m_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 (m_advanceCheck.isSelected()) { + onGetDestClasses = new Runnable() { + @Override + public void run() { + pickBestDestClass(); + } + }; + } + + setSourceClass(classEntry, onGetDestClasses); + } + + protected void setSourceClass(ClassEntry classEntry, final Runnable onGetDestClasses) { + + // update the current source class + m_sourceClass = classEntry; + m_sourceClassLabel.setText(m_sourceClass != null ? m_sourceClass.getName() : ""); + + if (m_sourceClass != null) { + + // show the dest class(es) + ClassMatch match = m_classMatches.getMatchBySource(m_sourceDeobfuscator.obfuscateEntry(m_sourceClass)); + assert(match != null); + if (match.destClasses.isEmpty()) { + + m_destClasses.setClasses(null); + + // run in a separate thread to keep ui responsive + new Thread() { + @Override + public void run() { + m_destClasses.setClasses(deobfuscateClasses(getLikelyMatches(m_sourceClass), m_destDeobfuscator)); + m_destClasses.expandAll(); + + if (onGetDestClasses != null) { + onGetDestClasses.run(); + } + } + }.start(); + + } else { + + m_destClasses.setClasses(deobfuscateClasses(match.destClasses, m_destDeobfuscator)); + m_destClasses.expandAll(); + + if (onGetDestClasses != null) { + onGetDestClasses.run(); + } + } + } + + setDestClass(null); + m_sourceReader.decompileClass(m_sourceClass, m_sourceDeobfuscator, new Runnable() { + @Override + public void run() { + m_sourceReader.navigateToClassDeclaration(m_sourceClass); + } + }); + + updateMatchButton(); + } + + private Collection getLikelyMatches(ClassEntry sourceClass) { + + ClassEntry obfSourceClass = m_sourceDeobfuscator.obfuscateEntry(sourceClass); + + // set up identifiers + ClassNamer namer = new ClassNamer(m_classMatches.getUniqueMatches()); + ClassIdentifier sourceIdentifier = new ClassIdentifier( + m_sourceDeobfuscator.getJar(), m_sourceDeobfuscator.getJarIndex(), + namer.getSourceNamer(), true + ); + ClassIdentifier destIdentifier = new ClassIdentifier( + m_destDeobfuscator.getJar(), m_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 : m_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)); + } + return scoredDestClasses; + + } catch (ClassNotFoundException ex) { + throw new Error("Unable to find class " + ex.getMessage()); + } + } + + protected void setDestClass(ClassEntry classEntry) { + + // update the current source class + m_destClass = classEntry; + m_destClassLabel.setText(m_destClass != null ? m_destClass.getName() : ""); + + m_destReader.decompileClass(m_destClass, m_destDeobfuscator, new Runnable() { + @Override + public void run() { + m_destReader.navigateToClassDeclaration(m_destClass); + } + }); + + updateMatchButton(); + } + + private void updateMatchButton() { + + ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass); + ClassEntry obfDest = m_destDeobfuscator.obfuscateEntry(m_destClass); + + BiMap uniqueMatches = m_classMatches.getUniqueMatches(); + boolean twoSelected = m_sourceClass != null && m_destClass != null; + boolean isMatched = uniqueMatches.containsKey(obfSource) && uniqueMatches.containsValue(obfDest); + boolean canMatch = !uniqueMatches.containsKey(obfSource) && ! uniqueMatches.containsValue(obfDest); + + GuiTricks.deactivateButton(m_matchButton); + if (twoSelected) { + if (isMatched) { + GuiTricks.activateButton(m_matchButton, "Unmatch", new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + onUnmatchClick(); + } + }); + } else if (canMatch) { + GuiTricks.activateButton(m_matchButton, "Match", new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + onMatchClick(); + } + }); + } + } + } + + private void onMatchClick() { + // precondition: source and dest classes are set correctly + + ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass); + ClassEntry obfDest = m_destDeobfuscator.obfuscateEntry(m_destClass); + + // remove the classes from their match + m_classMatches.removeSource(obfSource); + m_classMatches.removeDest(obfDest); + + // add them as matched classes + m_classMatches.add(new ClassMatch(obfSource, obfDest)); + + ClassEntry nextClass = null; + if (m_advanceCheck.isSelected()) { + nextClass = m_sourceClasses.getNextClass(m_sourceClass); + } + + save(); + updateMatches(); + + if (nextClass != null) { + advance(nextClass); + } + } + + private void onUnmatchClick() { + // precondition: source and dest classes are set to a unique match + + ClassEntry obfSource = m_sourceDeobfuscator.obfuscateEntry(m_sourceClass); + + // remove the source to break the match, then add the source back as unmatched + m_classMatches.removeSource(obfSource); + m_classMatches.add(new ClassMatch(obfSource, null)); + + save(); + updateMatches(); + } + + private void updateMatches() { + updateDestMappings(); + setDestClass(null); + m_destClasses.setClasses(null); + updateMatchButton(); + + // remember where we were in the source tree + String packageName = m_sourceClasses.getSelectedPackage(); + + setSourceType(m_sourceType); + + m_sourceClasses.expandPackage(packageName); + } + + private void save() { + if (m_saveListener != null) { + m_saveListener.save(m_classMatches); + } + } + + private void autoMatch() { + + System.out.println("Automatching..."); + + // compute a new matching + ClassMatching matching = MappingsConverter.computeMatching( + m_sourceDeobfuscator.getJar(), m_sourceDeobfuscator.getJarIndex(), + m_destDeobfuscator.getJar(), m_destDeobfuscator.getJarIndex(), + m_classMatches.getUniqueMatches() + ); + ClassMatches newMatches = new ClassMatches(matching.matches()); + System.out.println(String.format("Automatch found %d new matches", + newMatches.getUniqueMatches().size() - m_classMatches.getUniqueMatches().size() + )); + + // update the current matches + m_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 = m_sourceClasses.getSelectedClass(); + if (sourceClass != null) { + sourceClass = m_sourceClasses.getNextClass(sourceClass); + } else { + sourceClass = m_sourceClasses.getFirstClass(); + } + } + + // set the source class + setSourceClass(sourceClass, new Runnable() { + @Override + public void run() { + pickBestDestClass(); + } + }); + m_sourceClasses.setSelectionClass(sourceClass); + } + + private void pickBestDestClass() { + + // then, pick the best dest class + ClassEntry firstClass = null; + ScoredClassEntry bestDestClass = null; + for (ClassSelectorPackageNode packageNode : m_destClasses.packageNodes()) { + for (ClassSelectorClassNode classNode : m_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); + m_destClasses.setSelectionClass(destClass); + } +} diff --git a/src/cuchaz/enigma/gui/ClassSelector.java b/src/cuchaz/enigma/gui/ClassSelector.java new file mode 100644 index 0000000..11333a9 --- /dev/null +++ b/src/cuchaz/enigma/gui/ClassSelector.java @@ -0,0 +1,293 @@ +/******************************************************************************* + * 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 java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +import cuchaz.enigma.mapping.ClassEntry; + +public class ClassSelector extends JTree { + + private static final long serialVersionUID = -7632046902384775977L; + + public interface ClassSelectionListener { + void onSelectClass(ClassEntry classEntry); + } + + public static Comparator ObfuscatedClassEntryComparator; + public static Comparator DeobfuscatedClassEntryComparator; + + static { + ObfuscatedClassEntryComparator = new Comparator() { + @Override + public int compare(ClassEntry a, ClassEntry b) { + String aname = a.getName(); + String bname = a.getName(); + if (aname.length() != bname.length()) { + return aname.length() - bname.length(); + } + return aname.compareTo(bname); + } + }; + + DeobfuscatedClassEntryComparator = new Comparator() { + @Override + public int compare(ClassEntry a, ClassEntry b) { + if (a instanceof ScoredClassEntry && b instanceof ScoredClassEntry) { + return Float.compare( + ((ScoredClassEntry)b).getScore(), + ((ScoredClassEntry)a).getScore() + ); + } + return a.getName().compareTo(b.getName()); + } + }; + } + + private ClassSelectionListener m_listener; + private Comparator m_comparator; + + public ClassSelector(Comparator comparator) { + m_comparator = comparator; + + // configure the tree control + setRootVisible(false); + setShowsRootHandles(false); + setModel(null); + + // hook events + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (m_listener != null && event.getClickCount() == 2) { + // get the selected node + TreePath path = getSelectionPath(); + if (path != null && path.getLastPathComponent() instanceof ClassSelectorClassNode) { + ClassSelectorClassNode node = (ClassSelectorClassNode)path.getLastPathComponent(); + m_listener.onSelectClass(node.getClassEntry()); + } + } + } + }); + + // init defaults + m_listener = null; + } + + public void setListener(ClassSelectionListener val) { + m_listener = val; + } + + public void setClasses(Collection classEntries) { + if (classEntries == null) { + setModel(null); + return; + } + + // build the package names + Map packages = Maps.newHashMap(); + for (ClassEntry classEntry : classEntries) { + packages.put(classEntry.getPackageName(), null); + } + + // sort the packages + List sortedPackageNames = Lists.newArrayList(packages.keySet()); + Collections.sort(sortedPackageNames, new Comparator() { + @Override + public int compare(String a, String b) { + // I can never keep this rule straight when writing these damn things... + // a < b => -1, a == b => 0, a > b => +1 + + String[] aparts = a.split("/"); + String[] bparts = b.split("/"); + for (int i = 0; true; i++) { + if (i >= aparts.length) { + return -1; + } else if (i >= bparts.length) { + return 1; + } + + int result = aparts[i].compareTo(bparts[i]); + if (result != 0) { + return result; + } + } + } + }); + + // create the root node and the package nodes + DefaultMutableTreeNode root = new DefaultMutableTreeNode(); + for (String packageName : sortedPackageNames) { + ClassSelectorPackageNode node = new ClassSelectorPackageNode(packageName); + packages.put(packageName, node); + root.add(node); + } + + // put the classes into packages + Multimap packagedClassEntries = ArrayListMultimap.create(); + for (ClassEntry classEntry : classEntries) { + packagedClassEntries.put(classEntry.getPackageName(), classEntry); + } + + // build the class nodes + for (String packageName : packagedClassEntries.keySet()) { + // sort the class entries + List classEntriesInPackage = Lists.newArrayList(packagedClassEntries.get(packageName)); + Collections.sort(classEntriesInPackage, m_comparator); + + // create the nodes in order + for (ClassEntry classEntry : classEntriesInPackage) { + ClassSelectorPackageNode node = packages.get(packageName); + node.add(new ClassSelectorClassNode(classEntry)); + } + } + + // finally, update the tree control + setModel(new DefaultTreeModel(root)); + } + + public ClassEntry getSelectedClass() { + if (!isSelectionEmpty()) { + Object selectedNode = getSelectionPath().getLastPathComponent(); + if (selectedNode instanceof ClassSelectorClassNode) { + ClassSelectorClassNode classNode = (ClassSelectorClassNode)selectedNode; + return classNode.getClassEntry(); + } + } + return null; + } + + public String getSelectedPackage() { + if (!isSelectionEmpty()) { + Object selectedNode = getSelectionPath().getLastPathComponent(); + if (selectedNode instanceof ClassSelectorPackageNode) { + ClassSelectorPackageNode packageNode = (ClassSelectorPackageNode)selectedNode; + return packageNode.getPackageName(); + } else if (selectedNode instanceof ClassSelectorClassNode) { + ClassSelectorClassNode classNode = (ClassSelectorClassNode)selectedNode; + return classNode.getClassEntry().getPackageName(); + } + } + return null; + } + + public Iterable packageNodes() { + List nodes = Lists.newArrayList(); + DefaultMutableTreeNode root = (DefaultMutableTreeNode)getModel().getRoot(); + Enumeration children = root.children(); + while (children.hasMoreElements()) { + ClassSelectorPackageNode packageNode = (ClassSelectorPackageNode)children.nextElement(); + nodes.add(packageNode); + } + return nodes; + } + + public Iterable classNodes(ClassSelectorPackageNode packageNode) { + List nodes = Lists.newArrayList(); + Enumeration children = packageNode.children(); + while (children.hasMoreElements()) { + ClassSelectorClassNode classNode = (ClassSelectorClassNode)children.nextElement(); + nodes.add(classNode); + } + return nodes; + } + + public void expandPackage(String packageName) { + if (packageName == null) { + return; + } + for (ClassSelectorPackageNode packageNode : packageNodes()) { + if (packageNode.getPackageName().equals(packageName)) { + expandPath(new TreePath(new Object[] {getModel().getRoot(), packageNode})); + return; + } + } + } + + public void expandAll() { + for (ClassSelectorPackageNode packageNode : packageNodes()) { + expandPath(new TreePath(new Object[] {getModel().getRoot(), packageNode})); + } + } + + public ClassEntry getFirstClass() { + for (ClassSelectorPackageNode packageNode : packageNodes()) { + for (ClassSelectorClassNode classNode : classNodes(packageNode)) { + return classNode.getClassEntry(); + } + } + return null; + } + + public ClassSelectorPackageNode getPackageNode(ClassEntry entry) { + for (ClassSelectorPackageNode packageNode : packageNodes()) { + if (packageNode.getPackageName().equals(entry.getPackageName())) { + return packageNode; + } + } + return null; + } + + public ClassEntry getNextClass(ClassEntry entry) { + boolean foundIt = false; + for (ClassSelectorPackageNode packageNode : packageNodes()) { + if (!foundIt) { + // skip to the package with our target in it + if (packageNode.getPackageName().equals(entry.getPackageName())) { + for (ClassSelectorClassNode classNode : classNodes(packageNode)) { + if (!foundIt) { + if (classNode.getClassEntry().equals(entry)) { + foundIt = true; + } + } else { + // return the next class + return classNode.getClassEntry(); + } + } + } + } else { + // return the next class + for (ClassSelectorClassNode classNode : classNodes(packageNode)) { + return classNode.getClassEntry(); + } + } + } + return null; + } + + public void setSelectionClass(ClassEntry classEntry) { + expandPackage(classEntry.getPackageName()); + for (ClassSelectorPackageNode packageNode : packageNodes()) { + for (ClassSelectorClassNode classNode : classNodes(packageNode)) { + if (classNode.getClassEntry().equals(classEntry)) { + setSelectionPath(new TreePath(new Object[] {getModel().getRoot(), packageNode, classNode})); + } + } + } + } +} diff --git a/src/cuchaz/enigma/gui/ClassSelectorClassNode.java b/src/cuchaz/enigma/gui/ClassSelectorClassNode.java new file mode 100644 index 0000000..1219e89 --- /dev/null +++ b/src/cuchaz/enigma/gui/ClassSelectorClassNode.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * 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 javax.swing.tree.DefaultMutableTreeNode; + +import cuchaz.enigma.mapping.ClassEntry; + +public class ClassSelectorClassNode extends DefaultMutableTreeNode { + + private static final long serialVersionUID = -8956754339813257380L; + + private ClassEntry m_classEntry; + + public ClassSelectorClassNode(ClassEntry classEntry) { + m_classEntry = classEntry; + } + + public ClassEntry getClassEntry() { + return m_classEntry; + } + + @Override + public String toString() { + if (m_classEntry instanceof ScoredClassEntry) { + return String.format("%d%% %s", (int)((ScoredClassEntry)m_classEntry).getScore(), m_classEntry.getSimpleName()); + } + return m_classEntry.getSimpleName(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ClassSelectorClassNode) { + return equals((ClassSelectorClassNode)other); + } + return false; + } + + public boolean equals(ClassSelectorClassNode other) { + return m_classEntry.equals(other.m_classEntry); + } +} diff --git a/src/cuchaz/enigma/gui/ClassSelectorPackageNode.java b/src/cuchaz/enigma/gui/ClassSelectorPackageNode.java new file mode 100644 index 0000000..7259f54 --- /dev/null +++ b/src/cuchaz/enigma/gui/ClassSelectorPackageNode.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * 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 javax.swing.tree.DefaultMutableTreeNode; + +public class ClassSelectorPackageNode extends DefaultMutableTreeNode { + + private static final long serialVersionUID = -3730868701219548043L; + + private String m_packageName; + + public ClassSelectorPackageNode(String packageName) { + m_packageName = packageName; + } + + public String getPackageName() { + return m_packageName; + } + + @Override + public String toString() { + return m_packageName; + } + + @Override + public boolean equals(Object other) { + if (other instanceof ClassSelectorPackageNode) { + return equals((ClassSelectorPackageNode)other); + } + return false; + } + + public boolean equals(ClassSelectorPackageNode other) { + return m_packageName.equals(other.m_packageName); + } +} diff --git a/src/cuchaz/enigma/gui/CodeReader.java b/src/cuchaz/enigma/gui/CodeReader.java new file mode 100644 index 0000000..5033a2c --- /dev/null +++ b/src/cuchaz/enigma/gui/CodeReader.java @@ -0,0 +1,222 @@ +/******************************************************************************* + * 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 java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JEditorPane; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Highlighter.HighlightPainter; + +import com.strobel.decompiler.languages.java.ast.CompilationUnit; + +import cuchaz.enigma.Deobfuscator; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.SourceIndex; +import cuchaz.enigma.analysis.Token; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.Entry; +import de.sciss.syntaxpane.DefaultSyntaxKit; + + +public class CodeReader extends JEditorPane { + + private static final long serialVersionUID = 3673180950485748810L; + + private static final Object m_lock = new Object(); + + public static interface SelectionListener { + void onSelect(EntryReference reference); + } + + private SelectionHighlightPainter m_selectionHighlightPainter; + private SourceIndex m_sourceIndex; + private SelectionListener m_selectionListener; + + public CodeReader() { + + setEditable(false); + setContentType("text/java"); + + // turn off token highlighting (it's wrong most of the time anyway...) + DefaultSyntaxKit kit = (DefaultSyntaxKit)getEditorKit(); + kit.toggleComponent(this, "de.sciss.syntaxpane.components.TokenMarker"); + + // hook events + addCaretListener(new CaretListener() { + @Override + public void caretUpdate(CaretEvent event) { + if (m_selectionListener != null && m_sourceIndex != null) { + Token token = m_sourceIndex.getReferenceToken(event.getDot()); + if (token != null) { + m_selectionListener.onSelect(m_sourceIndex.getDeobfReference(token)); + } else { + m_selectionListener.onSelect(null); + } + } + } + }); + + m_selectionHighlightPainter = new SelectionHighlightPainter(); + m_sourceIndex = null; + m_selectionListener = null; + } + + public void setSelectionListener(SelectionListener val) { + m_selectionListener = val; + } + + public void setCode(String code) { + // sadly, the java lexer is not thread safe, so we have to serialize all these calls + synchronized (m_lock) { + setText(code); + } + } + + public SourceIndex getSourceIndex() { + return m_sourceIndex; + } + + public void decompileClass(ClassEntry classEntry, Deobfuscator deobfuscator) { + decompileClass(classEntry, deobfuscator, null); + } + + public void decompileClass(ClassEntry classEntry, Deobfuscator deobfuscator, Runnable callback) { + decompileClass(classEntry, deobfuscator, null, callback); + } + + public void decompileClass(final ClassEntry classEntry, final Deobfuscator deobfuscator, final Boolean ignoreBadTokens, final Runnable callback) { + + if (classEntry == null) { + setCode(null); + return; + } + + setCode("(decompiling...)"); + + // run decompilation in a separate thread to keep ui responsive + new Thread() { + @Override + public void run() { + + // decompile it + CompilationUnit sourceTree = deobfuscator.getSourceTree(classEntry.getOutermostClassName()); + String source = deobfuscator.getSource(sourceTree); + setCode(source); + m_sourceIndex = deobfuscator.getSourceIndex(sourceTree, source, ignoreBadTokens); + + if (callback != null) { + callback.run(); + } + } + }.start(); + } + + public void navigateToClassDeclaration(ClassEntry classEntry) { + + // navigate to the class declaration + Token token = m_sourceIndex.getDeclarationToken(classEntry); + if (token == null) { + // couldn't find the class declaration token, might be an anonymous class + // look for any declaration in that class instead + for (Entry entry : m_sourceIndex.declarations()) { + if (entry.getClassEntry().equals(classEntry)) { + token = m_sourceIndex.getDeclarationToken(entry); + break; + } + } + } + + if (token != null) { + navigateToToken(token); + } else { + // couldn't find anything =( + System.out.println("Unable to find declaration in source for " + classEntry); + } + } + + public void navigateToToken(final Token token) { + navigateToToken(this, token, m_selectionHighlightPainter); + } + + // HACKHACK: someday we can update the main GUI to use this code reader + public static void navigateToToken(final JEditorPane editor, final Token token, final HighlightPainter highlightPainter) { + + // set the caret position to the token + editor.setCaretPosition(token.start); + editor.grabFocus(); + + try { + // make sure the token is visible in the scroll window + Rectangle start = editor.modelToView(token.start); + Rectangle end = editor.modelToView(token.end); + final Rectangle show = start.union(end); + show.grow(start.width * 10, start.height * 6); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + editor.scrollRectToVisible(show); + } + }); + } catch (BadLocationException ex) { + throw new Error(ex); + } + + // highlight the token momentarily + final Timer timer = new Timer(200, new ActionListener() { + private int m_counter = 0; + private Object m_highlight = null; + + @Override + public void actionPerformed(ActionEvent event) { + if (m_counter % 2 == 0) { + try { + m_highlight = editor.getHighlighter().addHighlight(token.start, token.end, highlightPainter); + } catch (BadLocationException ex) { + // don't care + } + } else if (m_highlight != null) { + editor.getHighlighter().removeHighlight(m_highlight); + } + + if (m_counter++ > 6) { + Timer timer = (Timer)event.getSource(); + timer.stop(); + } + } + }); + timer.start(); + } + + public void setHighlightedTokens(Iterable tokens, HighlightPainter painter) { + for (Token token : tokens) { + setHighlightedToken(token, painter); + } + } + + public void setHighlightedToken(Token token, HighlightPainter painter) { + try { + getHighlighter().addHighlight(token.start, token.end, painter); + } catch (BadLocationException ex) { + throw new IllegalArgumentException(ex); + } + } + + public void clearHighlights() { + getHighlighter().removeAllHighlights(); + } +} diff --git a/src/cuchaz/enigma/gui/CrashDialog.java b/src/cuchaz/enigma/gui/CrashDialog.java new file mode 100644 index 0000000..904273c --- /dev/null +++ b/src/cuchaz/enigma/gui/CrashDialog.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * 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 java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.WindowConstants; + +import cuchaz.enigma.Constants; + +public class CrashDialog { + + private static CrashDialog m_instance = null; + + private JFrame m_frame; + private JTextArea m_text; + + private CrashDialog(JFrame parent) { + // init frame + m_frame = new JFrame(Constants.Name + " - Crash Report"); + final Container pane = m_frame.getContentPane(); + pane.setLayout(new BorderLayout()); + + JLabel label = new JLabel(Constants.Name + " has crashed! =("); + label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + pane.add(label, BorderLayout.NORTH); + + // report panel + m_text = new JTextArea(); + m_text.setTabSize(2); + pane.add(new JScrollPane(m_text), BorderLayout.CENTER); + + // buttons panel + JPanel buttonsPanel = new JPanel(); + FlowLayout buttonsLayout = new FlowLayout(); + buttonsLayout.setAlignment(FlowLayout.RIGHT); + buttonsPanel.setLayout(buttonsLayout); + buttonsPanel.add(GuiTricks.unboldLabel(new JLabel("If you choose exit, you will lose any unsaved work."))); + JButton ignoreButton = new JButton("Ignore"); + ignoreButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + // close (hide) the dialog + m_frame.setVisible(false); + } + }); + buttonsPanel.add(ignoreButton); + JButton exitButton = new JButton("Exit"); + exitButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + // exit enigma + System.exit(1); + } + }); + buttonsPanel.add(exitButton); + pane.add(buttonsPanel, BorderLayout.SOUTH); + + // show the frame + m_frame.setSize(600, 400); + m_frame.setLocationRelativeTo(parent); + m_frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + } + + public static void init(JFrame parent) { + m_instance = new CrashDialog(parent); + } + + public static void show(Throwable ex) { + // get the error report + StringWriter buf = new StringWriter(); + ex.printStackTrace(new PrintWriter(buf)); + String report = buf.toString(); + + // show it! + m_instance.m_text.setText(report); + m_instance.m_frame.doLayout(); + m_instance.m_frame.setVisible(true); + } +} diff --git a/src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java b/src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java new file mode 100644 index 0000000..57210a8 --- /dev/null +++ b/src/cuchaz/enigma/gui/DeobfuscatedHighlightPainter.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 java.awt.Color; + +public class DeobfuscatedHighlightPainter extends BoxHighlightPainter { + + public DeobfuscatedHighlightPainter() { + // green ish + super(new Color(220, 255, 220), new Color(80, 160, 80)); + } +} diff --git a/src/cuchaz/enigma/gui/Gui.java b/src/cuchaz/enigma/gui/Gui.java new file mode 100644 index 0000000..f9192d3 --- /dev/null +++ b/src/cuchaz/enigma/gui/Gui.java @@ -0,0 +1,1122 @@ +/******************************************************************************* + * 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 java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.InputEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Vector; +import java.util.jar.JarFile; + +import javax.swing.BorderFactory; +import javax.swing.JEditorPane; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.WindowConstants; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.text.BadLocationException; +import javax.swing.text.Highlighter; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import com.google.common.collect.Lists; + +import cuchaz.enigma.Constants; +import cuchaz.enigma.ExceptionIgnorer; +import cuchaz.enigma.analysis.BehaviorReferenceTreeNode; +import cuchaz.enigma.analysis.ClassImplementationsTreeNode; +import cuchaz.enigma.analysis.ClassInheritanceTreeNode; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.FieldReferenceTreeNode; +import cuchaz.enigma.analysis.MethodImplementationsTreeNode; +import cuchaz.enigma.analysis.MethodInheritanceTreeNode; +import cuchaz.enigma.analysis.ReferenceTreeNode; +import cuchaz.enigma.analysis.Token; +import cuchaz.enigma.gui.ClassSelector.ClassSelectionListener; +import cuchaz.enigma.mapping.ArgumentEntry; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.ConstructorEntry; +import cuchaz.enigma.mapping.Entry; +import cuchaz.enigma.mapping.FieldEntry; +import cuchaz.enigma.mapping.IllegalNameException; +import cuchaz.enigma.mapping.MappingParseException; +import cuchaz.enigma.mapping.MethodEntry; +import cuchaz.enigma.mapping.Signature; +import de.sciss.syntaxpane.DefaultSyntaxKit; + +public class Gui { + + private GuiController m_controller; + + // controls + private JFrame m_frame; + private ClassSelector m_obfClasses; + private ClassSelector m_deobfClasses; + private JEditorPane m_editor; + private JPanel m_classesPanel; + private JSplitPane m_splitClasses; + private JPanel m_infoPanel; + private ObfuscatedHighlightPainter m_obfuscatedHighlightPainter; + private DeobfuscatedHighlightPainter m_deobfuscatedHighlightPainter; + private OtherHighlightPainter m_otherHighlightPainter; + private SelectionHighlightPainter m_selectionHighlightPainter; + private JTree m_inheritanceTree; + private JTree m_implementationsTree; + private JTree m_callsTree; + private JList m_tokens; + private JTabbedPane m_tabs; + + // dynamic menu items + private JMenuItem m_closeJarMenu; + private JMenuItem m_openMappingsMenu; + private JMenuItem m_saveMappingsMenu; + private JMenuItem m_saveMappingsAsMenu; + private JMenuItem m_closeMappingsMenu; + private JMenuItem m_renameMenu; + private JMenuItem m_showInheritanceMenu; + private JMenuItem m_openEntryMenu; + private JMenuItem m_openPreviousMenu; + private JMenuItem m_showCallsMenu; + private JMenuItem m_showImplementationsMenu; + private JMenuItem m_toggleMappingMenu; + private JMenuItem m_exportSourceMenu; + private JMenuItem m_exportJarMenu; + + // state + private EntryReference m_reference; + private JFileChooser m_jarFileChooser; + private JFileChooser m_mappingsFileChooser; + private JFileChooser m_exportSourceFileChooser; + private JFileChooser m_exportJarFileChooser; + + public Gui() { + + // init frame + m_frame = new JFrame(Constants.Name); + final Container pane = m_frame.getContentPane(); + pane.setLayout(new BorderLayout()); + + if (Boolean.parseBoolean(System.getProperty("enigma.catchExceptions", "true"))) { + // install a global exception handler to the event thread + CrashDialog.init(m_frame); + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable t) { + t.printStackTrace(System.err); + if (!ExceptionIgnorer.shouldIgnore(t)) { + CrashDialog.show(t); + } + } + }); + } + + m_controller = new GuiController(this); + + // init file choosers + m_jarFileChooser = new JFileChooser(); + m_mappingsFileChooser = new JFileChooser(); + m_exportSourceFileChooser = new JFileChooser(); + m_exportSourceFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + m_exportJarFileChooser = new JFileChooser(); + + // init obfuscated classes list + m_obfClasses = new ClassSelector(ClassSelector.ObfuscatedClassEntryComparator); + m_obfClasses.setListener(new ClassSelectionListener() { + @Override + public void onSelectClass(ClassEntry classEntry) { + navigateTo(classEntry); + } + }); + JScrollPane obfScroller = new JScrollPane(m_obfClasses); + JPanel obfPanel = new JPanel(); + obfPanel.setLayout(new BorderLayout()); + obfPanel.add(new JLabel("Obfuscated Classes"), BorderLayout.NORTH); + obfPanel.add(obfScroller, BorderLayout.CENTER); + + // init deobfuscated classes list + m_deobfClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator); + m_deobfClasses.setListener(new ClassSelectionListener() { + @Override + public void onSelectClass(ClassEntry classEntry) { + navigateTo(classEntry); + } + }); + JScrollPane deobfScroller = new JScrollPane(m_deobfClasses); + JPanel deobfPanel = new JPanel(); + deobfPanel.setLayout(new BorderLayout()); + deobfPanel.add(new JLabel("De-obfuscated Classes"), BorderLayout.NORTH); + deobfPanel.add(deobfScroller, BorderLayout.CENTER); + + // set up classes panel (don't add the splitter yet) + m_splitClasses = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, obfPanel, deobfPanel); + m_splitClasses.setResizeWeight(0.3); + m_classesPanel = new JPanel(); + m_classesPanel.setLayout(new BorderLayout()); + m_classesPanel.setPreferredSize(new Dimension(250, 0)); + + // init info panel + m_infoPanel = new JPanel(); + m_infoPanel.setLayout(new GridLayout(4, 1, 0, 0)); + m_infoPanel.setPreferredSize(new Dimension(0, 100)); + m_infoPanel.setBorder(BorderFactory.createTitledBorder("Identifier Info")); + clearReference(); + + // init editor + DefaultSyntaxKit.initKit(); + m_obfuscatedHighlightPainter = new ObfuscatedHighlightPainter(); + m_deobfuscatedHighlightPainter = new DeobfuscatedHighlightPainter(); + m_otherHighlightPainter = new OtherHighlightPainter(); + m_selectionHighlightPainter = new SelectionHighlightPainter(); + m_editor = new JEditorPane(); + m_editor.setEditable(false); + m_editor.setCaret(new BrowserCaret()); + JScrollPane sourceScroller = new JScrollPane(m_editor); + m_editor.setContentType("text/java"); + m_editor.addCaretListener(new CaretListener() { + @Override + public void caretUpdate(CaretEvent event) { + onCaretMove(event.getDot()); + } + }); + m_editor.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + switch (event.getKeyCode()) { + case KeyEvent.VK_R: + m_renameMenu.doClick(); + break; + + case KeyEvent.VK_I: + m_showInheritanceMenu.doClick(); + break; + + case KeyEvent.VK_M: + m_showImplementationsMenu.doClick(); + break; + + case KeyEvent.VK_N: + m_openEntryMenu.doClick(); + break; + + case KeyEvent.VK_P: + m_openPreviousMenu.doClick(); + break; + + case KeyEvent.VK_C: + m_showCallsMenu.doClick(); + break; + + case KeyEvent.VK_T: + m_toggleMappingMenu.doClick(); + break; + } + } + }); + + // turn off token highlighting (it's wrong most of the time anyway...) + DefaultSyntaxKit kit = (DefaultSyntaxKit)m_editor.getEditorKit(); + kit.toggleComponent(m_editor, "de.sciss.syntaxpane.components.TokenMarker"); + + // init editor popup menu + JPopupMenu popupMenu = new JPopupMenu(); + m_editor.setComponentPopupMenu(popupMenu); + { + JMenuItem menu = new JMenuItem("Rename"); + menu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + startRename(); + } + }); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0)); + menu.setEnabled(false); + popupMenu.add(menu); + m_renameMenu = menu; + } + { + JMenuItem menu = new JMenuItem("Show Inheritance"); + menu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + showInheritance(); + } + }); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, 0)); + menu.setEnabled(false); + popupMenu.add(menu); + m_showInheritanceMenu = menu; + } + { + JMenuItem menu = new JMenuItem("Show Implementations"); + menu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + showImplementations(); + } + }); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, 0)); + menu.setEnabled(false); + popupMenu.add(menu); + m_showImplementationsMenu = menu; + } + { + JMenuItem menu = new JMenuItem("Show Calls"); + menu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + showCalls(); + } + }); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0)); + menu.setEnabled(false); + popupMenu.add(menu); + m_showCallsMenu = menu; + } + { + JMenuItem menu = new JMenuItem("Go to Declaration"); + menu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + navigateTo(m_reference.entry); + } + }); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, 0)); + menu.setEnabled(false); + popupMenu.add(menu); + m_openEntryMenu = menu; + } + { + JMenuItem menu = new JMenuItem("Go to previous"); + menu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + m_controller.openPreviousReference(); + } + }); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, 0)); + menu.setEnabled(false); + popupMenu.add(menu); + m_openPreviousMenu = menu; + } + { + JMenuItem menu = new JMenuItem("Mark as deobfuscated"); + menu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + toggleMapping(); + } + }); + menu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0)); + menu.setEnabled(false); + popupMenu.add(menu); + m_toggleMappingMenu = menu; + } + + // init inheritance panel + m_inheritanceTree = new JTree(); + m_inheritanceTree.setModel(null); + m_inheritanceTree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() == 2) { + // get the selected node + TreePath path = m_inheritanceTree.getSelectionPath(); + if (path == null) { + return; + } + + Object node = path.getLastPathComponent(); + if (node instanceof ClassInheritanceTreeNode) { + ClassInheritanceTreeNode classNode = (ClassInheritanceTreeNode)node; + navigateTo(new ClassEntry(classNode.getObfClassName())); + } else if (node instanceof MethodInheritanceTreeNode) { + MethodInheritanceTreeNode methodNode = (MethodInheritanceTreeNode)node; + if (methodNode.isImplemented()) { + navigateTo(methodNode.getMethodEntry()); + } + } + } + } + }); + JPanel inheritancePanel = new JPanel(); + inheritancePanel.setLayout(new BorderLayout()); + inheritancePanel.add(new JScrollPane(m_inheritanceTree)); + + // init implementations panel + m_implementationsTree = new JTree(); + m_implementationsTree.setModel(null); + m_implementationsTree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() == 2) { + // get the selected node + TreePath path = m_implementationsTree.getSelectionPath(); + if (path == null) { + return; + } + + Object node = path.getLastPathComponent(); + if (node instanceof ClassImplementationsTreeNode) { + ClassImplementationsTreeNode classNode = (ClassImplementationsTreeNode)node; + navigateTo(classNode.getClassEntry()); + } else if (node instanceof MethodImplementationsTreeNode) { + MethodImplementationsTreeNode methodNode = (MethodImplementationsTreeNode)node; + navigateTo(methodNode.getMethodEntry()); + } + } + } + }); + JPanel implementationsPanel = new JPanel(); + implementationsPanel.setLayout(new BorderLayout()); + implementationsPanel.add(new JScrollPane(m_implementationsTree)); + + // init call panel + m_callsTree = new JTree(); + m_callsTree.setModel(null); + m_callsTree.addMouseListener(new MouseAdapter() { + @SuppressWarnings("unchecked") + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() == 2) { + // get the selected node + TreePath path = m_callsTree.getSelectionPath(); + if (path == null) { + return; + } + + Object node = path.getLastPathComponent(); + if (node instanceof ReferenceTreeNode) { + ReferenceTreeNode referenceNode = ((ReferenceTreeNode)node); + if (referenceNode.getReference() != null) { + navigateTo(referenceNode.getReference()); + } else { + navigateTo(referenceNode.getEntry()); + } + } + } + } + }); + m_tokens = new JList(); + m_tokens.setCellRenderer(new TokenListCellRenderer(m_controller)); + m_tokens.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + m_tokens.setLayoutOrientation(JList.VERTICAL); + m_tokens.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent event) { + if (event.getClickCount() == 2) { + Token selected = m_tokens.getSelectedValue(); + if (selected != null) { + showToken(selected); + } + } + } + }); + m_tokens.setPreferredSize(new Dimension(0, 200)); + m_tokens.setMinimumSize(new Dimension(0, 200)); + JSplitPane callPanel = new JSplitPane( + JSplitPane.VERTICAL_SPLIT, + true, + new JScrollPane(m_callsTree), + new JScrollPane(m_tokens) + ); + callPanel.setResizeWeight(1); // let the top side take all the slack + callPanel.resetToPreferredSizes(); + + // layout controls + JPanel centerPanel = new JPanel(); + centerPanel.setLayout(new BorderLayout()); + centerPanel.add(m_infoPanel, BorderLayout.NORTH); + centerPanel.add(sourceScroller, BorderLayout.CENTER); + m_tabs = new JTabbedPane(); + m_tabs.setPreferredSize(new Dimension(250, 0)); + m_tabs.addTab("Inheritance", inheritancePanel); + m_tabs.addTab("Implementations", implementationsPanel); + m_tabs.addTab("Call Graph", callPanel); + JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, centerPanel, m_tabs); + splitRight.setResizeWeight(1); // let the left side take all the slack + splitRight.resetToPreferredSizes(); + JSplitPane splitCenter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, m_classesPanel, splitRight); + splitCenter.setResizeWeight(0); // let the right side take all the slack + pane.add(splitCenter, BorderLayout.CENTER); + + // init menus + JMenuBar menuBar = new JMenuBar(); + m_frame.setJMenuBar(menuBar); + { + JMenu menu = new JMenu("File"); + menuBar.add(menu); + { + JMenuItem item = new JMenuItem("Open Jar..."); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + if (m_jarFileChooser.showOpenDialog(m_frame) == JFileChooser.APPROVE_OPTION) { + // load the jar in a separate thread + new Thread() { + @Override + public void run() { + try { + m_controller.openJar(new JarFile(m_jarFileChooser.getSelectedFile())); + } catch (IOException ex) { + throw new Error(ex); + } + } + }.start(); + } + } + }); + } + { + JMenuItem item = new JMenuItem("Close Jar"); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + m_controller.closeJar(); + } + }); + m_closeJarMenu = item; + } + menu.addSeparator(); + { + JMenuItem item = new JMenuItem("Open Mappings..."); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + if (m_mappingsFileChooser.showOpenDialog(m_frame) == JFileChooser.APPROVE_OPTION) { + try { + m_controller.openMappings(m_mappingsFileChooser.getSelectedFile()); + } catch (IOException ex) { + throw new Error(ex); + } catch (MappingParseException ex) { + JOptionPane.showMessageDialog(m_frame, ex.getMessage()); + } + } + } + }); + m_openMappingsMenu = item; + } + { + JMenuItem item = new JMenuItem("Save Mappings"); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + try { + m_controller.saveMappings(m_mappingsFileChooser.getSelectedFile()); + } catch (IOException ex) { + throw new Error(ex); + } + } + }); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK)); + m_saveMappingsMenu = item; + } + { + JMenuItem item = new JMenuItem("Save Mappings As..."); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + if (m_mappingsFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) { + try { + m_controller.saveMappings(m_mappingsFileChooser.getSelectedFile()); + m_saveMappingsMenu.setEnabled(true); + } catch (IOException ex) { + throw new Error(ex); + } + } + } + }); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + m_saveMappingsAsMenu = item; + } + { + JMenuItem item = new JMenuItem("Close Mappings"); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + m_controller.closeMappings(); + } + }); + m_closeMappingsMenu = item; + } + menu.addSeparator(); + { + JMenuItem item = new JMenuItem("Export Source..."); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + if (m_exportSourceFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) { + m_controller.exportSource(m_exportSourceFileChooser.getSelectedFile()); + } + } + }); + m_exportSourceMenu = item; + } + { + JMenuItem item = new JMenuItem("Export Jar..."); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + if (m_exportJarFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) { + m_controller.exportJar(m_exportJarFileChooser.getSelectedFile()); + } + } + }); + m_exportJarMenu = item; + } + menu.addSeparator(); + { + JMenuItem item = new JMenuItem("Exit"); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + close(); + } + }); + } + } + { + JMenu menu = new JMenu("Help"); + menuBar.add(menu); + { + JMenuItem item = new JMenuItem("About"); + menu.add(item); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + AboutDialog.show(m_frame); + } + }); + } + } + + // init state + onCloseJar(); + + m_frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent event) { + close(); + } + }); + + // show the frame + pane.doLayout(); + m_frame.setSize(1024, 576); + m_frame.setMinimumSize(new Dimension(640, 480)); + m_frame.setVisible(true); + m_frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + } + + public JFrame getFrame() { + return m_frame; + } + + public GuiController getController() { + return m_controller; + } + + public void onStartOpenJar() { + m_classesPanel.removeAll(); + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout()); + panel.add(new JLabel("Loading...")); + m_classesPanel.add(panel); + redraw(); + } + + public void onFinishOpenJar(String jarName) { + // update gui + m_frame.setTitle(Constants.Name + " - " + jarName); + m_classesPanel.removeAll(); + m_classesPanel.add(m_splitClasses); + setSource(null); + + // update menu + m_closeJarMenu.setEnabled(true); + m_openMappingsMenu.setEnabled(true); + m_saveMappingsMenu.setEnabled(false); + m_saveMappingsAsMenu.setEnabled(true); + m_closeMappingsMenu.setEnabled(true); + m_exportSourceMenu.setEnabled(true); + m_exportJarMenu.setEnabled(true); + + redraw(); + } + + public void onCloseJar() { + // update gui + m_frame.setTitle(Constants.Name); + setObfClasses(null); + setDeobfClasses(null); + setSource(null); + m_classesPanel.removeAll(); + + // update menu + m_closeJarMenu.setEnabled(false); + m_openMappingsMenu.setEnabled(false); + m_saveMappingsMenu.setEnabled(false); + m_saveMappingsAsMenu.setEnabled(false); + m_closeMappingsMenu.setEnabled(false); + m_exportSourceMenu.setEnabled(false); + m_exportJarMenu.setEnabled(false); + + redraw(); + } + + public void setObfClasses(Collection obfClasses) { + m_obfClasses.setClasses(obfClasses); + } + + public void setDeobfClasses(Collection deobfClasses) { + m_deobfClasses.setClasses(deobfClasses); + } + + public void setMappingsFile(File file) { + m_mappingsFileChooser.setSelectedFile(file); + m_saveMappingsMenu.setEnabled(file != null); + } + + public void setSource(String source) { + m_editor.getHighlighter().removeAllHighlights(); + m_editor.setText(source); + } + + public void showToken(final Token token) { + if (token == null) { + throw new IllegalArgumentException("Token cannot be null!"); + } + CodeReader.navigateToToken(m_editor, token, m_selectionHighlightPainter); + redraw(); + } + + public void showTokens(Collection tokens) { + Vector sortedTokens = new Vector(tokens); + Collections.sort(sortedTokens); + if (sortedTokens.size() > 1) { + // sort the tokens and update the tokens panel + m_tokens.setListData(sortedTokens); + m_tokens.setSelectedIndex(0); + } else { + m_tokens.setListData(new Vector()); + } + + // show the first token + showToken(sortedTokens.get(0)); + } + + public void setHighlightedTokens(Iterable obfuscatedTokens, Iterable deobfuscatedTokens, Iterable otherTokens) { + + // remove any old highlighters + m_editor.getHighlighter().removeAllHighlights(); + + // color things based on the index + if (obfuscatedTokens != null) { + setHighlightedTokens(obfuscatedTokens, m_obfuscatedHighlightPainter); + } + if (deobfuscatedTokens != null) { + setHighlightedTokens(deobfuscatedTokens, m_deobfuscatedHighlightPainter); + } + if (otherTokens != null) { + setHighlightedTokens(otherTokens, m_otherHighlightPainter); + } + + redraw(); + } + + private void setHighlightedTokens(Iterable tokens, Highlighter.HighlightPainter painter) { + for (Token token : tokens) { + try { + m_editor.getHighlighter().addHighlight(token.start, token.end, painter); + } catch (BadLocationException ex) { + throw new IllegalArgumentException(ex); + } + } + } + + private void clearReference() { + m_infoPanel.removeAll(); + JLabel label = new JLabel("No identifier selected"); + GuiTricks.unboldLabel(label); + label.setHorizontalAlignment(JLabel.CENTER); + m_infoPanel.add(label); + + redraw(); + } + + private void showReference(EntryReference reference) { + if (reference == null) { + clearReference(); + return; + } + + m_reference = reference; + + m_infoPanel.removeAll(); + if (reference.entry instanceof ClassEntry) { + showClassEntry((ClassEntry)m_reference.entry); + } else if (m_reference.entry instanceof FieldEntry) { + showFieldEntry((FieldEntry)m_reference.entry); + } else if (m_reference.entry instanceof MethodEntry) { + showMethodEntry((MethodEntry)m_reference.entry); + } else if (m_reference.entry instanceof ConstructorEntry) { + showConstructorEntry((ConstructorEntry)m_reference.entry); + } else if (m_reference.entry instanceof ArgumentEntry) { + showArgumentEntry((ArgumentEntry)m_reference.entry); + } else { + throw new Error("Unknown entry type: " + m_reference.entry.getClass().getName()); + } + + redraw(); + } + + private void showClassEntry(ClassEntry entry) { + addNameValue(m_infoPanel, "Class", entry.getName()); + } + + private void showFieldEntry(FieldEntry entry) { + addNameValue(m_infoPanel, "Field", entry.getName()); + addNameValue(m_infoPanel, "Class", entry.getClassEntry().getName()); + addNameValue(m_infoPanel, "Type", entry.getType().toString()); + } + + private void showMethodEntry(MethodEntry entry) { + addNameValue(m_infoPanel, "Method", entry.getName()); + addNameValue(m_infoPanel, "Class", entry.getClassEntry().getName()); + addNameValue(m_infoPanel, "Signature", entry.getSignature().toString()); + } + + private void showConstructorEntry(ConstructorEntry entry) { + addNameValue(m_infoPanel, "Constructor", entry.getClassEntry().getName()); + if (!entry.isStatic()) { + addNameValue(m_infoPanel, "Signature", entry.getSignature().toString()); + } + } + + private void showArgumentEntry(ArgumentEntry entry) { + addNameValue(m_infoPanel, "Argument", entry.getName()); + addNameValue(m_infoPanel, "Class", entry.getClassEntry().getName()); + addNameValue(m_infoPanel, "Method", entry.getBehaviorEntry().getName()); + addNameValue(m_infoPanel, "Index", Integer.toString(entry.getIndex())); + } + + private void addNameValue(JPanel container, String name, String value) { + JPanel panel = new JPanel(); + panel.setLayout(new FlowLayout(FlowLayout.LEFT, 6, 0)); + container.add(panel); + + JLabel label = new JLabel(name + ":", JLabel.RIGHT); + label.setPreferredSize(new Dimension(100, label.getPreferredSize().height)); + panel.add(label); + + panel.add(GuiTricks.unboldLabel(new JLabel(value, JLabel.LEFT))); + } + + private void onCaretMove(int pos) { + + Token token = m_controller.getToken(pos); + boolean isToken = token != null; + + m_reference = m_controller.getDeobfReference(token); + boolean isClassEntry = isToken && m_reference.entry instanceof ClassEntry; + boolean isFieldEntry = isToken && m_reference.entry instanceof FieldEntry; + boolean isMethodEntry = isToken && m_reference.entry instanceof MethodEntry; + boolean isConstructorEntry = isToken && m_reference.entry instanceof ConstructorEntry; + boolean isInJar = isToken && m_controller.entryIsInJar(m_reference.entry); + boolean isRenameable = isToken && m_controller.referenceIsRenameable(m_reference); + + if (isToken) { + showReference(m_reference); + } else { + clearReference(); + } + + m_renameMenu.setEnabled(isRenameable && isToken); + m_showInheritanceMenu.setEnabled(isClassEntry || isMethodEntry || isConstructorEntry); + m_showImplementationsMenu.setEnabled(isClassEntry || isMethodEntry); + m_showCallsMenu.setEnabled(isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry); + m_openEntryMenu.setEnabled(isInJar && (isClassEntry || isFieldEntry || isMethodEntry || isConstructorEntry)); + m_openPreviousMenu.setEnabled(m_controller.hasPreviousLocation()); + m_toggleMappingMenu.setEnabled(isRenameable && isToken); + + if (isToken && m_controller.entryHasDeobfuscatedName(m_reference.entry)) { + m_toggleMappingMenu.setText("Reset to obfuscated"); + } else { + m_toggleMappingMenu.setText("Mark as deobfuscated"); + } + } + + private void navigateTo(Entry entry) { + if (!m_controller.entryIsInJar(entry)) { + // entry is not in the jar. Ignore it + return; + } + if (m_reference != null) { + m_controller.savePreviousReference(m_reference); + } + m_controller.openDeclaration(entry); + } + + private void navigateTo(EntryReference reference) { + if (!m_controller.entryIsInJar(reference.getLocationClassEntry())) { + // reference is not in the jar. Ignore it + return; + } + if (m_reference != null) { + m_controller.savePreviousReference(m_reference); + } + m_controller.openReference(reference); + } + + private void startRename() { + + // init the text box + final JTextField text = new JTextField(); + text.setText(m_reference.getNamableName()); + text.setPreferredSize(new Dimension(360, text.getPreferredSize().height)); + text.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + switch (event.getKeyCode()) { + case KeyEvent.VK_ENTER: + finishRename(text, true); + break; + + case KeyEvent.VK_ESCAPE: + finishRename(text, false); + break; + } + } + }); + + // find the label with the name and replace it with the text box + JPanel panel = (JPanel)m_infoPanel.getComponent(0); + panel.remove(panel.getComponentCount() - 1); + panel.add(text); + text.grabFocus(); + text.selectAll(); + + redraw(); + } + + private void finishRename(JTextField text, boolean saveName) { + String newName = text.getText(); + if (saveName && newName != null && newName.length() > 0) { + try { + m_controller.rename(m_reference, newName); + } catch (IllegalNameException ex) { + text.setBorder(BorderFactory.createLineBorder(Color.red, 1)); + text.setToolTipText(ex.getReason()); + GuiTricks.showToolTipNow(text); + } + return; + } + + // abort the rename + JPanel panel = (JPanel)m_infoPanel.getComponent(0); + panel.remove(panel.getComponentCount() - 1); + panel.add(GuiTricks.unboldLabel(new JLabel(m_reference.getNamableName(), JLabel.LEFT))); + + m_editor.grabFocus(); + + redraw(); + } + + private void showInheritance() { + + if (m_reference == null) { + return; + } + + m_inheritanceTree.setModel(null); + + if (m_reference.entry instanceof ClassEntry) { + // get the class inheritance + ClassInheritanceTreeNode classNode = m_controller.getClassInheritance((ClassEntry)m_reference.entry); + + // show the tree at the root + TreePath path = getPathToRoot(classNode); + m_inheritanceTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0))); + m_inheritanceTree.expandPath(path); + m_inheritanceTree.setSelectionRow(m_inheritanceTree.getRowForPath(path)); + } else if (m_reference.entry instanceof MethodEntry) { + // get the method inheritance + MethodInheritanceTreeNode classNode = m_controller.getMethodInheritance((MethodEntry)m_reference.entry); + + // show the tree at the root + TreePath path = getPathToRoot(classNode); + m_inheritanceTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0))); + m_inheritanceTree.expandPath(path); + m_inheritanceTree.setSelectionRow(m_inheritanceTree.getRowForPath(path)); + } + + m_tabs.setSelectedIndex(0); + redraw(); + } + + private void showImplementations() { + + if (m_reference == null) { + return; + } + + m_implementationsTree.setModel(null); + + if (m_reference.entry instanceof ClassEntry) { + // get the class implementations + ClassImplementationsTreeNode node = m_controller.getClassImplementations((ClassEntry)m_reference.entry); + if (node != null) { + // show the tree at the root + TreePath path = getPathToRoot(node); + m_implementationsTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0))); + m_implementationsTree.expandPath(path); + m_implementationsTree.setSelectionRow(m_implementationsTree.getRowForPath(path)); + } + } else if (m_reference.entry instanceof MethodEntry) { + // get the method implementations + MethodImplementationsTreeNode node = m_controller.getMethodImplementations((MethodEntry)m_reference.entry); + if (node != null) { + // show the tree at the root + TreePath path = getPathToRoot(node); + m_implementationsTree.setModel(new DefaultTreeModel((TreeNode)path.getPathComponent(0))); + m_implementationsTree.expandPath(path); + m_implementationsTree.setSelectionRow(m_implementationsTree.getRowForPath(path)); + } + } + + m_tabs.setSelectedIndex(1); + redraw(); + } + + private void showCalls() { + + if (m_reference == null) { + return; + } + + if (m_reference.entry instanceof ClassEntry) { + // look for calls to the default constructor + // TODO: get a list of all the constructors and find calls to all of them + BehaviorReferenceTreeNode node = m_controller.getMethodReferences(new ConstructorEntry((ClassEntry)m_reference.entry, new Signature("()V"))); + m_callsTree.setModel(new DefaultTreeModel(node)); + } else if (m_reference.entry instanceof FieldEntry) { + FieldReferenceTreeNode node = m_controller.getFieldReferences((FieldEntry)m_reference.entry); + m_callsTree.setModel(new DefaultTreeModel(node)); + } else if (m_reference.entry instanceof MethodEntry) { + BehaviorReferenceTreeNode node = m_controller.getMethodReferences((MethodEntry)m_reference.entry); + m_callsTree.setModel(new DefaultTreeModel(node)); + } else if (m_reference.entry instanceof ConstructorEntry) { + BehaviorReferenceTreeNode node = m_controller.getMethodReferences((ConstructorEntry)m_reference.entry); + m_callsTree.setModel(new DefaultTreeModel(node)); + } + + m_tabs.setSelectedIndex(2); + redraw(); + } + + private void toggleMapping() { + if (m_controller.entryHasDeobfuscatedName(m_reference.entry)) { + m_controller.removeMapping(m_reference); + } else { + m_controller.markAsDeobfuscated(m_reference); + } + } + + private TreePath getPathToRoot(TreeNode node) { + List nodes = Lists.newArrayList(); + TreeNode n = node; + do { + nodes.add(n); + n = n.getParent(); + } while (n != null); + Collections.reverse(nodes); + return new TreePath(nodes.toArray()); + } + + private void close() { + if (!m_controller.isDirty()) { + // everything is saved, we can exit safely + m_frame.dispose(); + } else { + // ask to save before closing + String[] options = { "Save and exit", "Discard changes", "Cancel" }; + int response = JOptionPane.showOptionDialog(m_frame, "Your mappings have not been saved yet. Do you want to save?", "Save your changes?", JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, null, options, options[2]); + switch (response) { + case JOptionPane.YES_OPTION: // save and exit + if (m_mappingsFileChooser.getSelectedFile() != null || m_mappingsFileChooser.showSaveDialog(m_frame) == JFileChooser.APPROVE_OPTION) { + try { + m_controller.saveMappings(m_mappingsFileChooser.getSelectedFile()); + m_frame.dispose(); + } catch (IOException ex) { + throw new Error(ex); + } + } + break; + + case JOptionPane.NO_OPTION: + // don't save, exit + m_frame.dispose(); + break; + + // cancel means do nothing + } + } + } + + private void redraw() { + m_frame.validate(); + m_frame.repaint(); + } +} diff --git a/src/cuchaz/enigma/gui/GuiController.java b/src/cuchaz/enigma/gui/GuiController.java new file mode 100644 index 0000000..6690622 --- /dev/null +++ b/src/cuchaz/enigma/gui/GuiController.java @@ -0,0 +1,358 @@ +/******************************************************************************* + * 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 java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collection; +import java.util.Deque; +import java.util.List; +import java.util.jar.JarFile; + +import com.google.common.collect.Lists; +import com.google.common.collect.Queues; +import com.strobel.decompiler.languages.java.ast.CompilationUnit; + +import cuchaz.enigma.Deobfuscator; +import cuchaz.enigma.Deobfuscator.ProgressListener; +import cuchaz.enigma.analysis.BehaviorReferenceTreeNode; +import cuchaz.enigma.analysis.ClassImplementationsTreeNode; +import cuchaz.enigma.analysis.ClassInheritanceTreeNode; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.FieldReferenceTreeNode; +import cuchaz.enigma.analysis.MethodImplementationsTreeNode; +import cuchaz.enigma.analysis.MethodInheritanceTreeNode; +import cuchaz.enigma.analysis.SourceIndex; +import cuchaz.enigma.analysis.Token; +import cuchaz.enigma.gui.ProgressDialog.ProgressRunnable; +import cuchaz.enigma.mapping.BehaviorEntry; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.Entry; +import cuchaz.enigma.mapping.FieldEntry; +import cuchaz.enigma.mapping.MappingParseException; +import cuchaz.enigma.mapping.MappingsReader; +import cuchaz.enigma.mapping.MappingsWriter; +import cuchaz.enigma.mapping.MethodEntry; +import cuchaz.enigma.mapping.TranslationDirection; + +public class GuiController { + + private Deobfuscator m_deobfuscator; + private Gui m_gui; + private SourceIndex m_index; + private ClassEntry m_currentObfClass; + private boolean m_isDirty; + private Deque> m_referenceStack; + + public GuiController(Gui gui) { + m_gui = gui; + m_deobfuscator = null; + m_index = null; + m_currentObfClass = null; + m_isDirty = false; + m_referenceStack = Queues.newArrayDeque(); + } + + public boolean isDirty() { + return m_isDirty; + } + + public void openJar(final JarFile jar) throws IOException { + m_gui.onStartOpenJar(); + m_deobfuscator = new Deobfuscator(jar); + m_gui.onFinishOpenJar(m_deobfuscator.getJarName()); + refreshClasses(); + } + + public void closeJar() { + m_deobfuscator = null; + m_gui.onCloseJar(); + } + + public void openMappings(File file) throws IOException, MappingParseException { + FileReader in = new FileReader(file); + m_deobfuscator.setMappings(new MappingsReader().read(in)); + in.close(); + m_isDirty = false; + m_gui.setMappingsFile(file); + refreshClasses(); + refreshCurrentClass(); + } + + public void saveMappings(File file) throws IOException { + FileWriter out = new FileWriter(file); + new MappingsWriter().write(out, m_deobfuscator.getMappings()); + out.close(); + m_isDirty = false; + } + + public void closeMappings() { + m_deobfuscator.setMappings(null); + m_gui.setMappingsFile(null); + refreshClasses(); + refreshCurrentClass(); + } + + public void exportSource(final File dirOut) { + ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable() { + @Override + public void run(ProgressListener progress) throws Exception { + m_deobfuscator.writeSources(dirOut, progress); + } + }); + } + + public void exportJar(final File fileOut) { + ProgressDialog.runInThread(m_gui.getFrame(), new ProgressRunnable() { + @Override + public void run(ProgressListener progress) { + m_deobfuscator.writeJar(fileOut, progress); + } + }); + } + + public Token getToken(int pos) { + if (m_index == null) { + return null; + } + return m_index.getReferenceToken(pos); + } + + public EntryReference getDeobfReference(Token token) { + if (m_index == null) { + return null; + } + return m_index.getDeobfReference(token); + } + + public ReadableToken getReadableToken(Token token) { + if (m_index == null) { + return null; + } + return new ReadableToken( + m_index.getLineNumber(token.start), + m_index.getColumnNumber(token.start), + m_index.getColumnNumber(token.end) + ); + } + + public boolean entryHasDeobfuscatedName(Entry deobfEntry) { + return m_deobfuscator.hasDeobfuscatedName(m_deobfuscator.obfuscateEntry(deobfEntry)); + } + + public boolean entryIsInJar(Entry deobfEntry) { + return m_deobfuscator.isObfuscatedIdentifier(m_deobfuscator.obfuscateEntry(deobfEntry)); + } + + public boolean referenceIsRenameable(EntryReference deobfReference) { + return m_deobfuscator.isRenameable(m_deobfuscator.obfuscateReference(deobfReference)); + } + + public ClassInheritanceTreeNode getClassInheritance(ClassEntry deobfClassEntry) { + ClassEntry obfClassEntry = m_deobfuscator.obfuscateEntry(deobfClassEntry); + ClassInheritanceTreeNode rootNode = m_deobfuscator.getJarIndex().getClassInheritance( + m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating), + obfClassEntry + ); + return ClassInheritanceTreeNode.findNode(rootNode, obfClassEntry); + } + + public ClassImplementationsTreeNode getClassImplementations(ClassEntry deobfClassEntry) { + ClassEntry obfClassEntry = m_deobfuscator.obfuscateEntry(deobfClassEntry); + return m_deobfuscator.getJarIndex().getClassImplementations( + m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating), + obfClassEntry + ); + } + + public MethodInheritanceTreeNode getMethodInheritance(MethodEntry deobfMethodEntry) { + MethodEntry obfMethodEntry = m_deobfuscator.obfuscateEntry(deobfMethodEntry); + MethodInheritanceTreeNode rootNode = m_deobfuscator.getJarIndex().getMethodInheritance( + m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating), + obfMethodEntry + ); + return MethodInheritanceTreeNode.findNode(rootNode, obfMethodEntry); + } + + public MethodImplementationsTreeNode getMethodImplementations(MethodEntry deobfMethodEntry) { + MethodEntry obfMethodEntry = m_deobfuscator.obfuscateEntry(deobfMethodEntry); + List rootNodes = m_deobfuscator.getJarIndex().getMethodImplementations( + m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating), + obfMethodEntry + ); + if (rootNodes.isEmpty()) { + return null; + } + if (rootNodes.size() > 1) { + System.err.println("WARNING: Method " + deobfMethodEntry + " implements multiple interfaces. Only showing first one."); + } + return MethodImplementationsTreeNode.findNode(rootNodes.get(0), obfMethodEntry); + } + + public FieldReferenceTreeNode getFieldReferences(FieldEntry deobfFieldEntry) { + FieldEntry obfFieldEntry = m_deobfuscator.obfuscateEntry(deobfFieldEntry); + FieldReferenceTreeNode rootNode = new FieldReferenceTreeNode( + m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating), + obfFieldEntry + ); + rootNode.load(m_deobfuscator.getJarIndex(), true); + return rootNode; + } + + public BehaviorReferenceTreeNode getMethodReferences(BehaviorEntry deobfBehaviorEntry) { + BehaviorEntry obfBehaviorEntry = m_deobfuscator.obfuscateEntry(deobfBehaviorEntry); + BehaviorReferenceTreeNode rootNode = new BehaviorReferenceTreeNode( + m_deobfuscator.getTranslator(TranslationDirection.Deobfuscating), + obfBehaviorEntry + ); + rootNode.load(m_deobfuscator.getJarIndex(), true); + return rootNode; + } + + public void rename(EntryReference deobfReference, String newName) { + EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference); + m_deobfuscator.rename(obfReference.getNameableEntry(), newName); + m_isDirty = true; + refreshClasses(); + refreshCurrentClass(obfReference); + } + + public void removeMapping(EntryReference deobfReference) { + EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference); + m_deobfuscator.removeMapping(obfReference.getNameableEntry()); + m_isDirty = true; + refreshClasses(); + refreshCurrentClass(obfReference); + } + + public void markAsDeobfuscated(EntryReference deobfReference) { + EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference); + m_deobfuscator.markAsDeobfuscated(obfReference.getNameableEntry()); + m_isDirty = true; + refreshClasses(); + refreshCurrentClass(obfReference); + } + + public void openDeclaration(Entry deobfEntry) { + if (deobfEntry == null) { + throw new IllegalArgumentException("Entry cannot be null!"); + } + openReference(new EntryReference(deobfEntry, deobfEntry.getName())); + } + + public void openReference(EntryReference deobfReference) { + if (deobfReference == null) { + throw new IllegalArgumentException("Reference cannot be null!"); + } + + // get the reference target class + EntryReference obfReference = m_deobfuscator.obfuscateReference(deobfReference); + ClassEntry obfClassEntry = obfReference.getLocationClassEntry().getOutermostClassEntry(); + if (!m_deobfuscator.isObfuscatedIdentifier(obfClassEntry)) { + throw new IllegalArgumentException("Obfuscated class " + obfClassEntry + " was not found in the jar!"); + } + if (m_currentObfClass == null || !m_currentObfClass.equals(obfClassEntry)) { + // deobfuscate the class, then navigate to the reference + m_currentObfClass = obfClassEntry; + deobfuscate(m_currentObfClass, obfReference); + } else { + showReference(obfReference); + } + } + + private void showReference(EntryReference obfReference) { + EntryReference deobfReference = m_deobfuscator.deobfuscateReference(obfReference); + Collection tokens = m_index.getReferenceTokens(deobfReference); + if (tokens.isEmpty()) { + // DEBUG + System.err.println(String.format("WARNING: no tokens found for %s in %s", deobfReference, m_currentObfClass)); + } else { + m_gui.showTokens(tokens); + } + } + + public void savePreviousReference(EntryReference deobfReference) { + m_referenceStack.push(m_deobfuscator.obfuscateReference(deobfReference)); + } + + public void openPreviousReference() { + if (hasPreviousLocation()) { + openReference(m_deobfuscator.deobfuscateReference(m_referenceStack.pop())); + } + } + + public boolean hasPreviousLocation() { + return !m_referenceStack.isEmpty(); + } + + private void refreshClasses() { + List obfClasses = Lists.newArrayList(); + List deobfClasses = Lists.newArrayList(); + m_deobfuscator.getSeparatedClasses(obfClasses, deobfClasses); + m_gui.setObfClasses(obfClasses); + m_gui.setDeobfClasses(deobfClasses); + } + + private void refreshCurrentClass() { + refreshCurrentClass(null); + } + + private void refreshCurrentClass(EntryReference obfReference) { + if (m_currentObfClass != null) { + deobfuscate(m_currentObfClass, obfReference); + } + } + + private void deobfuscate(final ClassEntry classEntry, final EntryReference obfReference) { + + m_gui.setSource("(deobfuscating...)"); + + // run the deobfuscator in a separate thread so we don't block the GUI event queue + new Thread() { + @Override + public void run() { + // decompile,deobfuscate the bytecode + CompilationUnit sourceTree = m_deobfuscator.getSourceTree(classEntry.getClassName()); + if (sourceTree == null) { + // decompilation of this class is not supported + m_gui.setSource("Unable to find class: " + classEntry); + return; + } + String source = m_deobfuscator.getSource(sourceTree); + m_index = m_deobfuscator.getSourceIndex(sourceTree, source); + m_gui.setSource(m_index.getSource()); + if (obfReference != null) { + showReference(obfReference); + } + + // set the highlighted tokens + List obfuscatedTokens = Lists.newArrayList(); + List deobfuscatedTokens = Lists.newArrayList(); + List otherTokens = Lists.newArrayList(); + for (Token token : m_index.referenceTokens()) { + EntryReference reference = m_index.getDeobfReference(token); + if (referenceIsRenameable(reference)) { + if (entryHasDeobfuscatedName(reference.getNameableEntry())) { + deobfuscatedTokens.add(token); + } else { + obfuscatedTokens.add(token); + } + } else { + otherTokens.add(token); + } + } + m_gui.setHighlightedTokens(obfuscatedTokens, deobfuscatedTokens, otherTokens); + } + }.start(); + } +} diff --git a/src/cuchaz/enigma/gui/GuiTricks.java b/src/cuchaz/enigma/gui/GuiTricks.java new file mode 100644 index 0000000..5dc3ffb --- /dev/null +++ b/src/cuchaz/enigma/gui/GuiTricks.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * 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 java.awt.Font; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.util.Arrays; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.ToolTipManager; + +public class GuiTricks { + + public static JLabel unboldLabel(JLabel label) { + Font font = label.getFont(); + label.setFont(font.deriveFont(font.getStyle() & ~Font.BOLD)); + return label; + } + + public static void showToolTipNow(JComponent component) { + // HACKHACK: trick the tooltip manager into showing the tooltip right now + ToolTipManager manager = ToolTipManager.sharedInstance(); + int oldDelay = manager.getInitialDelay(); + manager.setInitialDelay(0); + manager.mouseMoved(new MouseEvent(component, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, 0, 0, 0, false)); + manager.setInitialDelay(oldDelay); + } + + public static void deactivateButton(JButton button) { + button.setEnabled(false); + button.setText(""); + for (ActionListener listener : Arrays.asList(button.getActionListeners())) { + button.removeActionListener(listener); + } + } + + public static void activateButton(JButton button, String text, ActionListener newListener) { + button.setText(text); + button.setEnabled(true); + for (ActionListener listener : Arrays.asList(button.getActionListeners())) { + button.removeActionListener(listener); + } + button.addActionListener(newListener); + } +} diff --git a/src/cuchaz/enigma/gui/MemberMatchingGui.java b/src/cuchaz/enigma/gui/MemberMatchingGui.java new file mode 100644 index 0000000..150eaad --- /dev/null +++ b/src/cuchaz/enigma/gui/MemberMatchingGui.java @@ -0,0 +1,499 @@ +/******************************************************************************* + * 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 java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.WindowConstants; +import javax.swing.text.Highlighter.HighlightPainter; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import cuchaz.enigma.Constants; +import cuchaz.enigma.Deobfuscator; +import cuchaz.enigma.analysis.EntryReference; +import cuchaz.enigma.analysis.SourceIndex; +import cuchaz.enigma.analysis.Token; +import cuchaz.enigma.convert.ClassMatches; +import cuchaz.enigma.convert.MemberMatches; +import cuchaz.enigma.gui.ClassSelector.ClassSelectionListener; +import cuchaz.enigma.mapping.ClassEntry; +import cuchaz.enigma.mapping.Entry; +import de.sciss.syntaxpane.DefaultSyntaxKit; + + +public class MemberMatchingGui { + + private static enum SourceType { + Matched { + + @Override + public Collection getObfSourceClasses(MemberMatches matches) { + return matches.getSourceClassesWithoutUnmatchedEntries(); + } + }, + Unmatched { + + @Override + public Collection getObfSourceClasses(MemberMatches matches) { + return matches.getSourceClassesWithUnmatchedEntries(); + } + }; + + 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 getObfSourceClasses(MemberMatches matches); + + public static SourceType getDefault() { + return values()[0]; + } + } + + public static interface SaveListener { + public void save(MemberMatches matches); + } + + // controls + private JFrame m_frame; + private Map m_sourceTypeButtons; + private ClassSelector m_sourceClasses; + private CodeReader m_sourceReader; + private CodeReader m_destReader; + private JButton m_matchButton; + private JButton m_unmatchableButton; + private JLabel m_sourceLabel; + private JLabel m_destLabel; + private HighlightPainter m_unmatchedHighlightPainter; + private HighlightPainter m_matchedHighlightPainter; + + private ClassMatches m_classMatches; + private MemberMatches m_memberMatches; + private Deobfuscator m_sourceDeobfuscator; + private Deobfuscator m_destDeobfuscator; + private SaveListener m_saveListener; + private SourceType m_sourceType; + private ClassEntry m_obfSourceClass; + private ClassEntry m_obfDestClass; + private T m_obfSourceEntry; + private T m_obfDestEntry; + + public MemberMatchingGui(ClassMatches classMatches, MemberMatches fieldMatches, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) { + + m_classMatches = classMatches; + m_memberMatches = fieldMatches; + m_sourceDeobfuscator = sourceDeobfuscator; + m_destDeobfuscator = destDeobfuscator; + + // init frame + m_frame = new JFrame(Constants.Name + " - Member Matcher"); + final Container pane = m_frame.getContentPane(); + pane.setLayout(new BorderLayout()); + + // init classes side + JPanel classesPanel = new JPanel(); + classesPanel.setLayout(new BoxLayout(classesPanel, BoxLayout.PAGE_AXIS)); + classesPanel.setPreferredSize(new Dimension(200, 0)); + pane.add(classesPanel, BorderLayout.WEST); + classesPanel.add(new JLabel("Classes")); + + // init source type radios + JPanel sourceTypePanel = new JPanel(); + classesPanel.add(sourceTypePanel); + sourceTypePanel.setLayout(new BoxLayout(sourceTypePanel, BoxLayout.PAGE_AXIS)); + ActionListener sourceTypeListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + setSourceType(SourceType.valueOf(event.getActionCommand())); + } + }; + ButtonGroup sourceTypeButtons = new ButtonGroup(); + m_sourceTypeButtons = Maps.newHashMap(); + for (SourceType sourceType : SourceType.values()) { + JRadioButton button = sourceType.newRadio(sourceTypeListener, sourceTypeButtons); + m_sourceTypeButtons.put(sourceType, button); + sourceTypePanel.add(button); + } + + m_sourceClasses = new ClassSelector(ClassSelector.DeobfuscatedClassEntryComparator); + m_sourceClasses.setListener(new ClassSelectionListener() { + @Override + public void onSelectClass(ClassEntry classEntry) { + setSourceClass(classEntry); + } + }); + JScrollPane sourceScroller = new JScrollPane(m_sourceClasses); + classesPanel.add(sourceScroller); + + // init readers + DefaultSyntaxKit.initKit(); + m_sourceReader = new CodeReader(); + m_sourceReader.setSelectionListener(new CodeReader.SelectionListener() { + @Override + public void onSelect(EntryReference reference) { + if (reference != null) { + onSelectSource(reference.entry); + } else { + onSelectSource(null); + } + } + }); + m_destReader = new CodeReader(); + m_destReader.setSelectionListener(new CodeReader.SelectionListener() { + @Override + public void onSelect(EntryReference reference) { + if (reference != null) { + onSelectDest(reference.entry); + } else { + onSelectDest(null); + } + } + }); + + // add key bindings + KeyAdapter keyListener = new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + switch (event.getKeyCode()) { + case KeyEvent.VK_M: + m_matchButton.doClick(); + break; + } + } + }; + m_sourceReader.addKeyListener(keyListener); + m_destReader.addKeyListener(keyListener); + + // init all the splits + JSplitPane splitRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, new JScrollPane(m_sourceReader), new JScrollPane(m_destReader)); + splitRight.setResizeWeight(0.5); // resize 50:50 + JSplitPane splitLeft = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, classesPanel, splitRight); + splitLeft.setResizeWeight(0); // let the right side take all the slack + pane.add(splitLeft, BorderLayout.CENTER); + splitLeft.resetToPreferredSizes(); + + // init bottom panel + JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout(new FlowLayout()); + pane.add(bottomPanel, BorderLayout.SOUTH); + + m_matchButton = new JButton(); + m_unmatchableButton = new JButton(); + + m_sourceLabel = new JLabel(); + bottomPanel.add(m_sourceLabel); + bottomPanel.add(m_matchButton); + bottomPanel.add(m_unmatchableButton); + m_destLabel = new JLabel(); + bottomPanel.add(m_destLabel); + + // show the frame + pane.doLayout(); + m_frame.setSize(1024, 576); + m_frame.setMinimumSize(new Dimension(640, 480)); + m_frame.setVisible(true); + m_frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + + m_unmatchedHighlightPainter = new ObfuscatedHighlightPainter(); + m_matchedHighlightPainter = new DeobfuscatedHighlightPainter(); + + // init state + m_saveListener = null; + m_obfSourceClass = null; + m_obfDestClass = null; + m_obfSourceEntry = null; + m_obfDestEntry = null; + setSourceType(SourceType.getDefault()); + updateButtons(); + } + + protected void setSourceType(SourceType val) { + m_sourceType = val; + updateSourceClasses(); + } + + public void setSaveListener(SaveListener val) { + m_saveListener = val; + } + + private void updateSourceClasses() { + + String selectedPackage = m_sourceClasses.getSelectedPackage(); + + List deobfClassEntries = Lists.newArrayList(); + for (ClassEntry entry : m_sourceType.getObfSourceClasses(m_memberMatches)) { + deobfClassEntries.add(m_sourceDeobfuscator.deobfuscateEntry(entry)); + } + m_sourceClasses.setClasses(deobfClassEntries); + + if (selectedPackage != null) { + m_sourceClasses.expandPackage(selectedPackage); + } + + for (SourceType sourceType : SourceType.values()) { + m_sourceTypeButtons.get(sourceType).setText(String.format("%s (%d)", + sourceType.name(), sourceType.getObfSourceClasses(m_memberMatches).size() + )); + } + } + + protected void setSourceClass(ClassEntry sourceClass) { + + m_obfSourceClass = m_sourceDeobfuscator.obfuscateEntry(sourceClass); + m_obfDestClass = m_classMatches.getUniqueMatches().get(m_obfSourceClass); + if (m_obfDestClass == null) { + throw new Error("No matching dest class for source class: " + m_obfSourceClass); + } + + m_sourceReader.decompileClass(m_obfSourceClass, m_sourceDeobfuscator, false, new Runnable() { + @Override + public void run() { + updateSourceHighlights(); + } + }); + m_destReader.decompileClass(m_obfDestClass, m_destDeobfuscator, false, new Runnable() { + @Override + public void run() { + updateDestHighlights(); + } + }); + } + + protected void updateSourceHighlights() { + highlightEntries(m_sourceReader, m_sourceDeobfuscator, m_memberMatches.matches().keySet(), m_memberMatches.getUnmatchedSourceEntries()); + } + + protected void updateDestHighlights() { + highlightEntries(m_destReader, m_destDeobfuscator, m_memberMatches.matches().values(), m_memberMatches.getUnmatchedDestEntries()); + } + + private void highlightEntries(CodeReader reader, Deobfuscator deobfuscator, Collection obfMatchedEntries, Collection obfUnmatchedEntries) { + reader.clearHighlights(); + SourceIndex index = reader.getSourceIndex(); + + // matched fields + for (T obfT : obfMatchedEntries) { + T deobfT = deobfuscator.deobfuscateEntry(obfT); + Token token = index.getDeclarationToken(deobfT); + if (token != null) { + reader.setHighlightedToken(token, m_matchedHighlightPainter); + } + } + + // unmatched fields + for (T obfT : obfUnmatchedEntries) { + T deobfT = deobfuscator.deobfuscateEntry(obfT); + Token token = index.getDeclarationToken(deobfT); + if (token != null) { + reader.setHighlightedToken(token, m_unmatchedHighlightPainter); + } + } + } + + private boolean isSelectionMatched() { + return m_obfSourceEntry != null && m_obfDestEntry != null + && m_memberMatches.isMatched(m_obfSourceEntry, m_obfDestEntry); + } + + protected void onSelectSource(Entry source) { + + // start with no selection + if (isSelectionMatched()) { + setDest(null); + } + setSource(null); + + // then look for a valid source selection + if (source != null) { + + // this looks really scary, but it's actually ok + // Deobfuscator.obfuscateEntry can handle all implementations of Entry + // and MemberMatches.hasSource() will only pass entries that actually match T + @SuppressWarnings("unchecked") + T sourceEntry = (T)source; + + T obfSourceEntry = m_sourceDeobfuscator.obfuscateEntry(sourceEntry); + if (m_memberMatches.hasSource(obfSourceEntry)) { + setSource(obfSourceEntry); + + // look for a matched dest too + T obfDestEntry = m_memberMatches.matches().get(obfSourceEntry); + if (obfDestEntry != null) { + setDest(obfDestEntry); + } + } + } + + updateButtons(); + } + + protected void onSelectDest(Entry dest) { + + // start with no selection + if (isSelectionMatched()) { + setSource(null); + } + setDest(null); + + // then look for a valid dest selection + if (dest != null) { + + // this looks really scary, but it's actually ok + // Deobfuscator.obfuscateEntry can handle all implementations of Entry + // and MemberMatches.hasSource() will only pass entries that actually match T + @SuppressWarnings("unchecked") + T destEntry = (T)dest; + + T obfDestEntry = m_destDeobfuscator.obfuscateEntry(destEntry); + if (m_memberMatches.hasDest(obfDestEntry)) { + setDest(obfDestEntry); + + // look for a matched source too + T obfSourceEntry = m_memberMatches.matches().inverse().get(obfDestEntry); + if (obfSourceEntry != null) { + setSource(obfSourceEntry); + } + } + } + + updateButtons(); + } + + private void setSource(T obfEntry) { + if (obfEntry == null) { + m_obfSourceEntry = obfEntry; + m_sourceLabel.setText(""); + } else { + m_obfSourceEntry = obfEntry; + m_sourceLabel.setText(getEntryLabel(obfEntry, m_sourceDeobfuscator)); + } + } + + private void setDest(T obfEntry) { + if (obfEntry == null) { + m_obfDestEntry = obfEntry; + m_destLabel.setText(""); + } else { + m_obfDestEntry = obfEntry; + m_destLabel.setText(getEntryLabel(obfEntry, m_destDeobfuscator)); + } + } + + private String getEntryLabel(T obfEntry, Deobfuscator deobfuscator) { + // show obfuscated and deobfuscated names, but no types/signatures + T deobfEntry = deobfuscator.deobfuscateEntry(obfEntry); + return String.format("%s (%s)", deobfEntry.getName(), obfEntry.getName()); + } + + private void updateButtons() { + + GuiTricks.deactivateButton(m_matchButton); + GuiTricks.deactivateButton(m_unmatchableButton); + + if (m_obfSourceEntry != null && m_obfDestEntry != null) { + if (m_memberMatches.isMatched(m_obfSourceEntry, m_obfDestEntry)) { + GuiTricks.activateButton(m_matchButton, "Unmatch", new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + unmatch(); + } + }); + } else if (!m_memberMatches.isMatchedSourceEntry(m_obfSourceEntry) && !m_memberMatches.isMatchedDestEntry(m_obfDestEntry)) { + GuiTricks.activateButton(m_matchButton, "Match", new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + match(); + } + }); + } + } else if (m_obfSourceEntry != null) { + GuiTricks.activateButton(m_unmatchableButton, "Set Unmatchable", new ActionListener() { + @Override + public void actionPerformed(ActionEvent event) { + unmatchable(); + } + }); + } + } + + protected void match() { + + // update the field matches + m_memberMatches.makeMatch(m_obfSourceEntry, m_obfDestEntry); + save(); + + // update the ui + onSelectSource(null); + onSelectDest(null); + updateSourceHighlights(); + updateDestHighlights(); + updateSourceClasses(); + } + + protected void unmatch() { + + // update the field matches + m_memberMatches.unmakeMatch(m_obfSourceEntry, m_obfDestEntry); + save(); + + // update the ui + onSelectSource(null); + onSelectDest(null); + updateSourceHighlights(); + updateDestHighlights(); + updateSourceClasses(); + } + + protected void unmatchable() { + + // update the field matches + m_memberMatches.makeSourceUnmatchable(m_obfSourceEntry); + save(); + + // update the ui + onSelectSource(null); + onSelectDest(null); + updateSourceHighlights(); + updateDestHighlights(); + updateSourceClasses(); + } + + private void save() { + if (m_saveListener != null) { + m_saveListener.save(m_memberMatches); + } + } +} diff --git a/src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java b/src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java new file mode 100644 index 0000000..4c3714a --- /dev/null +++ b/src/cuchaz/enigma/gui/ObfuscatedHighlightPainter.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 java.awt.Color; + +public class ObfuscatedHighlightPainter extends BoxHighlightPainter { + + public ObfuscatedHighlightPainter() { + // red ish + super(new Color(255, 220, 220), new Color(160, 80, 80)); + } +} diff --git a/src/cuchaz/enigma/gui/OtherHighlightPainter.java b/src/cuchaz/enigma/gui/OtherHighlightPainter.java new file mode 100644 index 0000000..8d3fbe8 --- /dev/null +++ b/src/cuchaz/enigma/gui/OtherHighlightPainter.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * 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 java.awt.Color; + +public class OtherHighlightPainter extends BoxHighlightPainter { + + public OtherHighlightPainter() { + // grey + super(null, new Color(180, 180, 180)); + } +} diff --git a/src/cuchaz/enigma/gui/ProgressDialog.java b/src/cuchaz/enigma/gui/ProgressDialog.java new file mode 100644 index 0000000..1c20f10 --- /dev/null +++ b/src/cuchaz/enigma/gui/ProgressDialog.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * 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 java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; + +import javax.swing.BorderFactory; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.WindowConstants; + +import cuchaz.enigma.Constants; +import cuchaz.enigma.Deobfuscator.ProgressListener; + +public class ProgressDialog implements ProgressListener, AutoCloseable { + + private JFrame m_frame; + private JLabel m_title; + private JLabel m_text; + private JProgressBar m_progress; + + public ProgressDialog(JFrame parent) { + + // init frame + m_frame = new JFrame(Constants.Name + " - Operation in progress"); + final Container pane = m_frame.getContentPane(); + FlowLayout layout = new FlowLayout(); + layout.setAlignment(FlowLayout.LEFT); + pane.setLayout(layout); + + m_title = new JLabel(); + pane.add(m_title); + + // set up the progress bar + JPanel panel = new JPanel(); + pane.add(panel); + panel.setLayout(new BorderLayout()); + m_text = GuiTricks.unboldLabel(new JLabel()); + m_progress = new JProgressBar(); + m_text.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + panel.add(m_text, BorderLayout.NORTH); + panel.add(m_progress, BorderLayout.CENTER); + panel.setPreferredSize(new Dimension(360, 50)); + + // show the frame + pane.doLayout(); + m_frame.setSize(400, 120); + m_frame.setResizable(false); + m_frame.setLocationRelativeTo(parent); + m_frame.setVisible(true); + m_frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + } + + public void close() { + m_frame.dispose(); + } + + @Override + public void init(int totalWork, String title) { + m_title.setText(title); + m_progress.setMinimum(0); + m_progress.setMaximum(totalWork); + m_progress.setValue(0); + } + + @Override + public void onProgress(int numDone, String message) { + m_text.setText(message); + m_progress.setValue(numDone); + + // update the frame + m_frame.validate(); + m_frame.repaint(); + } + + public static interface ProgressRunnable { + void run(ProgressListener listener) throws Exception; + } + + public static void runInThread(final JFrame parent, final ProgressRunnable runnable) { + new Thread() { + @Override + public void run() { + try (ProgressDialog progress = new ProgressDialog(parent)) { + runnable.run(progress); + } catch (Exception ex) { + throw new Error(ex); + } + } + }.start(); + } +} diff --git a/src/cuchaz/enigma/gui/ReadableToken.java b/src/cuchaz/enigma/gui/ReadableToken.java new file mode 100644 index 0000000..0741af3 --- /dev/null +++ b/src/cuchaz/enigma/gui/ReadableToken.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * 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; + +public class ReadableToken { + + public int line; + public int startColumn; + public int endColumn; + + public ReadableToken(int line, int startColumn, int endColumn) { + this.line = line; + this.startColumn = startColumn; + this.endColumn = endColumn; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append("line "); + buf.append(line); + buf.append(" columns "); + buf.append(startColumn); + buf.append("-"); + buf.append(endColumn); + return buf.toString(); + } +} diff --git a/src/cuchaz/enigma/gui/RenameListener.java b/src/cuchaz/enigma/gui/RenameListener.java new file mode 100644 index 0000000..8b515bb --- /dev/null +++ b/src/cuchaz/enigma/gui/RenameListener.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * 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 cuchaz.enigma.mapping.Entry; + +public interface RenameListener { + void rename(Entry obfEntry, String newName); +} diff --git a/src/cuchaz/enigma/gui/ScoredClassEntry.java b/src/cuchaz/enigma/gui/ScoredClassEntry.java new file mode 100644 index 0000000..6070452 --- /dev/null +++ b/src/cuchaz/enigma/gui/ScoredClassEntry.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * 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 cuchaz.enigma.mapping.ClassEntry; + + +public class ScoredClassEntry extends ClassEntry { + + private static final long serialVersionUID = -8798725308554217105L; + + private float m_score; + + public ScoredClassEntry(ClassEntry other, float score) { + super(other); + m_score = score; + } + + public float getScore() { + return m_score; + } +} diff --git a/src/cuchaz/enigma/gui/SelectionHighlightPainter.java b/src/cuchaz/enigma/gui/SelectionHighlightPainter.java new file mode 100644 index 0000000..4165da4 --- /dev/null +++ b/src/cuchaz/enigma/gui/SelectionHighlightPainter.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * 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 java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; + +import javax.swing.text.Highlighter; +import javax.swing.text.JTextComponent; + +public class SelectionHighlightPainter implements Highlighter.HighlightPainter { + + @Override + public void paint(Graphics g, int start, int end, Shape shape, JTextComponent text) { + // draw a thick border + Graphics2D g2d = (Graphics2D)g; + Rectangle bounds = BoxHighlightPainter.getBounds(text, start, end); + g2d.setColor(Color.black); + g2d.setStroke(new BasicStroke(2.0f)); + g2d.drawRoundRect(bounds.x, bounds.y, bounds.width, bounds.height, 4, 4); + } +} diff --git a/src/cuchaz/enigma/gui/TokenListCellRenderer.java b/src/cuchaz/enigma/gui/TokenListCellRenderer.java new file mode 100644 index 0000000..e4f7c87 --- /dev/null +++ b/src/cuchaz/enigma/gui/TokenListCellRenderer.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * 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 java.awt.Component; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.ListCellRenderer; + +import cuchaz.enigma.analysis.Token; + +public class TokenListCellRenderer implements ListCellRenderer { + + private GuiController m_controller; + private DefaultListCellRenderer m_defaultRenderer; + + public TokenListCellRenderer(GuiController controller) { + m_controller = controller; + m_defaultRenderer = new DefaultListCellRenderer(); + } + + @Override + public Component getListCellRendererComponent(JList list, Token token, int index, boolean isSelected, boolean hasFocus) { + JLabel label = (JLabel)m_defaultRenderer.getListCellRendererComponent(list, token, index, isSelected, hasFocus); + label.setText(m_controller.getReadableToken(token).toString()); + return label; + } +} -- cgit v1.2.3