+ * Contributors:
+ * Jeff Martin - initial API and implementation
+ ******************************************************************************/
+package cuchaz.enigma;
+
+import java.io.File;
+import java.io.FileReader;
+import java.util.jar.JarFile;
+
+import cuchaz.enigma.Deobfuscator.ProgressListener;
+import cuchaz.enigma.mapping.Mappings;
+import cuchaz.enigma.mapping.MappingsReader;
+
+public class CommandMain {
+
+ public static class ConsoleProgressListener implements ProgressListener {
+
+ private static final int ReportTime = 5000; // 5s
+
+ private int m_totalWork;
+ private long m_startTime;
+ private long m_lastReportTime;
+
+ @Override
+ public void init(int totalWork, String title) {
+ m_totalWork = totalWork;
+ m_startTime = System.currentTimeMillis();
+ m_lastReportTime = m_startTime;
+ System.out.println(title);
+ }
+
+ @Override
+ public void onProgress(int numDone, String message) {
+
+ long now = System.currentTimeMillis();
+ boolean isLastUpdate = numDone == m_totalWork;
+ boolean shouldReport = isLastUpdate || now - m_lastReportTime > ReportTime;
+
+ if (shouldReport) {
+ int percent = numDone * 100 / m_totalWork;
+ System.out.println(String.format("\tProgress: %3d%%", percent));
+ m_lastReportTime = now;
+ }
+ if (isLastUpdate) {
+ double elapsedSeconds = (now - m_startTime) / 1000;
+ System.out.println(String.format("Finished in %.1f seconds", elapsedSeconds));
+ }
+ }
+ }
+
+ public static void main(String[] args)
+ throws Exception {
+
+ try {
+
+ // process the command
+ String command = getArg(args, 0, "command", true);
+ if (command.equalsIgnoreCase("deobfuscate")) {
+ deobfuscate(args);
+ } else if (command.equalsIgnoreCase("decompile")) {
+ decompile(args);
+ } else if (command.equalsIgnoreCase("protectify")) {
+ protectify(args);
+ } else if (command.equalsIgnoreCase("publify")) {
+ publify(args);
+ } else {
+ throw new IllegalArgumentException("Command not recognized: " + command);
+ }
+ } catch (IllegalArgumentException ex) {
+ System.out.println(ex.getMessage());
+ printHelp();
+ }
+ }
+
+ private static void printHelp() {
+ System.out.println(String.format("%s - %s", Constants.Name, Constants.Version));
+ System.out.println("Usage:");
+ System.out.println("\tjava -cp enigma.jar cuchaz.enigma.CommandMain ");
+ System.out.println("\twhere is one of:");
+ System.out.println("\t\tdeobfuscate []");
+ System.out.println("\t\tdecompile []");
+ System.out.println("\t\tprotectify ");
+ }
+
+ private static void decompile(String[] args)
+ throws Exception {
+ File fileJarIn = getReadableFile(getArg(args, 1, "in jar", true));
+ File fileJarOut = getWritableFolder(getArg(args, 2, "out folder", true));
+ File fileMappings = getReadableFile(getArg(args, 3, "mappings file", false));
+ Deobfuscator deobfuscator = getDeobfuscator(fileMappings, new JarFile(fileJarIn));
+ deobfuscator.writeSources(fileJarOut, new ConsoleProgressListener());
+ }
+
+ private static void deobfuscate(String[] args)
+ throws Exception {
+ File fileJarIn = getReadableFile(getArg(args, 1, "in jar", true));
+ File fileJarOut = getWritableFile(getArg(args, 2, "out jar", true));
+ File fileMappings = getReadableFile(getArg(args, 3, "mappings file", false));
+ Deobfuscator deobfuscator = getDeobfuscator(fileMappings, new JarFile(fileJarIn));
+ deobfuscator.writeJar(fileJarOut, new ConsoleProgressListener());
+ }
+
+ private static void protectify(String[] args)
+ throws Exception {
+ File fileJarIn = getReadableFile(getArg(args, 1, "in jar", true));
+ File fileJarOut = getWritableFile(getArg(args, 2, "out jar", true));
+ Deobfuscator deobfuscator = getDeobfuscator(null, new JarFile(fileJarIn));
+ deobfuscator.protectifyJar(fileJarOut, new ConsoleProgressListener());
+ }
+
+ private static void publify(String[] args)
+ throws Exception {
+ File fileJarIn = getReadableFile(getArg(args, 1, "in jar", true));
+ File fileJarOut = getWritableFile(getArg(args, 2, "out jar", true));
+ Deobfuscator deobfuscator = getDeobfuscator(null, new JarFile(fileJarIn));
+ deobfuscator.publifyJar(fileJarOut, new ConsoleProgressListener());
+ }
+
+ private static Deobfuscator getDeobfuscator(File fileMappings, JarFile jar)
+ throws Exception {
+ System.out.println("Reading jar...");
+ Deobfuscator deobfuscator = new Deobfuscator(jar);
+ if (fileMappings != null) {
+ System.out.println("Reading mappings...");
+ Mappings mappings = new MappingsReader().read(fileMappings);
+ deobfuscator.setMappings(mappings);
+ }
+ return deobfuscator;
+ }
+
+ private static String getArg(String[] args, int i, String name, boolean required) {
+ if (i >= args.length) {
+ if (required) {
+ throw new IllegalArgumentException(name + " is required");
+ } else {
+ return null;
+ }
+ }
+ return args[i];
+ }
+
+ private static File getWritableFile(String path) {
+ if (path == null) {
+ return null;
+ }
+ File file = new File(path).getAbsoluteFile();
+ File dir = file.getParentFile();
+ if (dir == null) {
+ throw new IllegalArgumentException("Cannot write to folder: " + dir);
+ }
+ // quick fix to avoid stupid stuff in Gradle code
+ if (!dir.isDirectory()) {
+ dir.mkdirs();
+ }
+ return file;
+ }
+
+ private static File getWritableFolder(String path) {
+ if (path == null) {
+ return null;
+ }
+ File dir = new File(path).getAbsoluteFile();
+ if (!dir.exists()) {
+ throw new IllegalArgumentException("Cannot write to folder: " + dir);
+ }
+ return dir;
+ }
+
+ private static File getReadableFile(String path) {
+ if (path == null) {
+ return null;
+ }
+ File file = new File(path).getAbsoluteFile();
+ if (!file.exists()) {
+ throw new IllegalArgumentException("Cannot find file: " + file.getAbsolutePath());
+ }
+ return file;
+ }
+}
diff --git a/src/main/java/cuchaz/enigma/Constants.java b/src/main/java/cuchaz/enigma/Constants.java
new file mode 100644
index 00000000..5d2c84bf
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/Constants.java
@@ -0,0 +1,20 @@
+/*******************************************************************************
+ * 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;
+
+public class Constants {
+ public static final String Name = "Enigma";
+ public static final String Version = "0.2 - Beta";
+ public static final String Url = "http://www.cuchazinteractive.com/enigma";
+ public static final int MiB = 1024 * 1024; // 1 mebibyte
+ public static final int KiB = 1024; // 1 kebibyte
+ public static final String NonePackage = "none";
+}
diff --git a/src/main/java/cuchaz/enigma/ConvertMain.java b/src/main/java/cuchaz/enigma/ConvertMain.java
new file mode 100644
index 00000000..409a2692
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/ConvertMain.java
@@ -0,0 +1,362 @@
+/*******************************************************************************
+ * 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;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.jar.JarFile;
+
+import cuchaz.enigma.convert.*;
+import cuchaz.enigma.gui.ClassMatchingGui;
+import cuchaz.enigma.gui.MemberMatchingGui;
+import cuchaz.enigma.mapping.*;
+
+
+public class ConvertMain {
+
+ public static void main(String[] args)
+ throws IOException, MappingParseException {
+ try {
+ //Get all are args
+ String JarOld = getArg(args, 1, "Path to Old Jar", true);
+ String JarNew = getArg(args, 2, "Path to New Jar", true);
+ String OldMappings = getArg(args, 3, "Path to old .mappings file", true);
+ String NewMappings = getArg(args, 4, "Path to new .mappings file", true);
+ String ClassMatches = getArg(args, 5, "Path to Class .matches file", true);
+ String FieldMatches = getArg(args, 6, "Path to Field .matches file", true);
+ String MethodMatches = getArg(args, 7, "Path to Method .matches file", true);
+ //OldJar
+ JarFile sourceJar = new JarFile(new File(JarOld));
+ //NewJar
+ JarFile destJar = new JarFile(new File(JarNew));
+ //Get the mapping files
+ File inMappingsFile = new File(OldMappings);
+ File outMappingsFile = new File(NewMappings);
+ Mappings mappings = new MappingsReader().read( inMappingsFile);
+ //Make the Match Files..
+ File classMatchesFile = new File(ClassMatches);
+ File fieldMatchesFile = new File(FieldMatches);
+ File methodMatchesFile = new File(MethodMatches);
+
+ String command = getArg(args, 0, "command", true);
+
+ if (command.equalsIgnoreCase("computeClassMatches")) {
+ computeClassMatches(classMatchesFile, sourceJar, destJar, mappings);
+ convertMappings(outMappingsFile, sourceJar, destJar, mappings, classMatchesFile);
+ } else if (command.equalsIgnoreCase("editClassMatches")) {
+ editClasssMatches(classMatchesFile, sourceJar, destJar, mappings);
+ convertMappings(outMappingsFile, sourceJar, destJar, mappings, classMatchesFile);
+ } else if (command.equalsIgnoreCase("computeFieldMatches")) {
+ computeFieldMatches(fieldMatchesFile, destJar, outMappingsFile, classMatchesFile);
+ convertMappings(outMappingsFile, sourceJar, destJar, mappings, classMatchesFile, fieldMatchesFile);
+ } else if (command.equalsIgnoreCase("editFieldMatches")) {
+ editFieldMatches(sourceJar, destJar, outMappingsFile, mappings, classMatchesFile, fieldMatchesFile);
+ convertMappings(outMappingsFile, sourceJar, destJar, mappings, classMatchesFile, fieldMatchesFile);
+ } else if (command.equalsIgnoreCase("computeMethodMatches")) {
+ computeMethodMatches(methodMatchesFile, destJar, outMappingsFile, classMatchesFile);
+ convertMappings(outMappingsFile, sourceJar, destJar, mappings, classMatchesFile, fieldMatchesFile, methodMatchesFile);
+ } else if (command.equalsIgnoreCase("editMethodMatches")) {
+ editMethodMatches(sourceJar, destJar, outMappingsFile, mappings, classMatchesFile, methodMatchesFile);
+ convertMappings(outMappingsFile, sourceJar, destJar, mappings, classMatchesFile, fieldMatchesFile, methodMatchesFile);
+ } else if (command.equalsIgnoreCase("convertMappings")) {
+ convertMappings(outMappingsFile, sourceJar, destJar, mappings, classMatchesFile, fieldMatchesFile, methodMatchesFile);
+ }
+ } catch (IllegalArgumentException ex) {
+ System.out.println(ex.getMessage());
+ printHelp();
+ }
+ }
+
+ private static void printHelp() {
+ System.out.println(String.format("%s - %s", Constants.Name, Constants.Version));
+ System.out.println("Usage:");
+ System.out.println("\tjava -cp enigma.jar cuchaz.enigma.ConvertMain ");
+ System.out.println("\tWhere is one of:");
+ System.out.println("\t\tcomputeClassMatches");
+ System.out.println("\t\teditClassMatches");
+ System.out.println("\t\tcomputeFieldMatches");
+ System.out.println("\t\teditFieldMatches");
+ System.out.println("\t\teditMethodMatches");
+ System.out.println("\t\tconvertMappings");
+ System.out.println("\tWhere is the already mapped jar.");
+ System.out.println("\tWhere is the unmapped jar.");
+ System.out.println("\tWhere is the path to the mappings for the old jar.");
+ System.out.println("\tWhere is the new mappings. (Where you want to save them and there name)");
+ System.out.println("\tWhere is the class matches file.");
+ System.out.println("\tWhere is the field matches file.");
+ System.out.println("\tWhere is the method matches file.");
+ }
+
+ //Copy of getArg from CommandMain.... Should make a utils class.
+ private static String getArg(String[] args, int i, String name, boolean required) {
+ if (i >= args.length) {
+ if (required) {
+ throw new IllegalArgumentException(name + " is required");
+ } else {
+ return null;
+ }
+ }
+ return args[i];
+ }
+
+ private static void computeClassMatches(File classMatchesFile, JarFile sourceJar, JarFile destJar, Mappings mappings)
+ throws IOException {
+ ClassMatches classMatches = MappingsConverter.computeClassMatches(sourceJar, destJar, mappings);
+ MatchesWriter.writeClasses(classMatches, classMatchesFile);
+ System.out.println("Wrote:\n\t" + classMatchesFile.getAbsolutePath());
+ }
+
+ private static void editClasssMatches(final File classMatchesFile, JarFile sourceJar, JarFile destJar, Mappings mappings)
+ throws IOException {
+ System.out.println("Reading class matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ Deobfuscators deobfuscators = new Deobfuscators(sourceJar, destJar);
+ deobfuscators.source.setMappings(mappings);
+ System.out.println("Starting GUI...");
+ new ClassMatchingGui(classMatches, deobfuscators.source, deobfuscators.dest).setSaveListener(new ClassMatchingGui.SaveListener() {
+ @Override
+ public void save(ClassMatches matches) {
+ try {
+ MatchesWriter.writeClasses(matches, classMatchesFile);
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("unused")
+ private static void convertMappings(File outMappingsFile, JarFile sourceJar, JarFile destJar, Mappings mappings, File classMatchesFile)
+ throws IOException {
+ System.out.println("Reading class matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ Deobfuscators deobfuscators = new Deobfuscators(sourceJar, destJar);
+ deobfuscators.source.setMappings(mappings);
+
+ Mappings newMappings = MappingsConverter.newMappings(classMatches, mappings, deobfuscators.source, deobfuscators.dest);
+ new MappingsWriter().write(outMappingsFile, newMappings);
+ System.out.println("Write converted mappings to: " + outMappingsFile.getAbsolutePath());
+ }
+
+ private static void computeFieldMatches(File memberMatchesFile, JarFile destJar, File destMappingsFile, File classMatchesFile)
+ throws IOException, MappingParseException {
+
+ System.out.println("Reading class matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ System.out.println("Reading mappings...");
+ Mappings destMappings = new MappingsReader().read(destMappingsFile);
+ System.out.println("Indexing dest jar...");
+ Deobfuscator destDeobfuscator = new Deobfuscator(destJar);
+
+ System.out.println("Writing matches...");
+
+ // get the matched and unmatched mappings
+ MemberMatches fieldMatches = MappingsConverter.computeMemberMatches(
+ destDeobfuscator,
+ destMappings,
+ classMatches,
+ MappingsConverter.getFieldDoer()
+ );
+
+ MatchesWriter.writeMembers(fieldMatches, memberMatchesFile);
+ System.out.println("Wrote:\n\t" + memberMatchesFile.getAbsolutePath());
+ }
+
+ private static void editFieldMatches(JarFile sourceJar, JarFile destJar, File destMappingsFile, Mappings sourceMappings, File classMatchesFile, final File fieldMatchesFile)
+ throws IOException, MappingParseException {
+
+ System.out.println("Reading matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ MemberMatches fieldMatches = MatchesReader.readMembers(fieldMatchesFile);
+
+ // prep deobfuscators
+ Deobfuscators deobfuscators = new Deobfuscators(sourceJar, destJar);
+ deobfuscators.source.setMappings(sourceMappings);
+ Mappings destMappings = new MappingsReader().read(destMappingsFile);
+ MappingsChecker checker = new MappingsChecker(deobfuscators.dest.getJarIndex());
+ checker.dropBrokenMappings(destMappings);
+ deobfuscators.dest.setMappings(destMappings);
+
+ new MemberMatchingGui(classMatches, fieldMatches, deobfuscators.source, deobfuscators.dest).setSaveListener(new MemberMatchingGui.SaveListener() {
+ @Override
+ public void save(MemberMatches matches) {
+ try {
+ MatchesWriter.writeMembers(matches, fieldMatchesFile);
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("unused")
+ private static void convertMappings(File outMappingsFile, JarFile sourceJar, JarFile destJar, Mappings mappings, File classMatchesFile, File fieldMatchesFile)
+ throws IOException {
+
+ System.out.println("Reading matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ MemberMatches fieldMatches = MatchesReader.readMembers(fieldMatchesFile);
+
+ Deobfuscators deobfuscators = new Deobfuscators(sourceJar, destJar);
+ deobfuscators.source.setMappings(mappings);
+
+ // apply matches
+ Mappings newMappings = MappingsConverter.newMappings(classMatches, mappings, deobfuscators.source, deobfuscators.dest);
+ MappingsConverter.applyMemberMatches(newMappings, classMatches, fieldMatches, MappingsConverter.getFieldDoer());
+
+ // write out the converted mappings
+
+ new MappingsWriter().write(outMappingsFile, newMappings);
+ System.out.println("Wrote converted mappings to:\n\t" + outMappingsFile.getAbsolutePath());
+ }
+
+
+ private static void computeMethodMatches(File methodMatchesFile, JarFile destJar, File destMappingsFile, File classMatchesFile)
+ throws IOException, MappingParseException {
+
+ System.out.println("Reading class matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ System.out.println("Reading mappings...");
+ Mappings destMappings = new MappingsReader().read(destMappingsFile);
+ System.out.println("Indexing dest jar...");
+ Deobfuscator destDeobfuscator = new Deobfuscator(destJar);
+
+ System.out.println("Writing method matches...");
+
+ // get the matched and unmatched mappings
+ MemberMatches methodMatches = MappingsConverter.computeMemberMatches(
+ destDeobfuscator,
+ destMappings,
+ classMatches,
+ MappingsConverter.getMethodDoer()
+ );
+
+ MatchesWriter.writeMembers(methodMatches, methodMatchesFile);
+ System.out.println("Wrote:\n\t" + methodMatchesFile.getAbsolutePath());
+ }
+
+ private static void editMethodMatches(JarFile sourceJar, JarFile destJar, File destMappingsFile, Mappings sourceMappings, File classMatchesFile, final File methodMatchesFile)
+ throws IOException, MappingParseException {
+
+ System.out.println("Reading matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ MemberMatches methodMatches = MatchesReader.readMembers(methodMatchesFile);
+
+ // prep deobfuscators
+ Deobfuscators deobfuscators = new Deobfuscators(sourceJar, destJar);
+ deobfuscators.source.setMappings(sourceMappings);
+ Mappings destMappings = new MappingsReader().read(destMappingsFile);
+ MappingsChecker checker = new MappingsChecker(deobfuscators.dest.getJarIndex());
+ checker.dropBrokenMappings(destMappings);
+ deobfuscators.dest.setMappings(destMappings);
+
+ new MemberMatchingGui(classMatches, methodMatches, deobfuscators.source, deobfuscators.dest).setSaveListener(new MemberMatchingGui.SaveListener() {
+ @Override
+ public void save(MemberMatches matches) {
+ try {
+ MatchesWriter.writeMembers(matches, methodMatchesFile);
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ });
+ }
+
+ private static void convertMappings(File outMappingsFile, JarFile sourceJar, JarFile destJar, Mappings mappings, File classMatchesFile, File fieldMatchesFile, File methodMatchesFile)
+ throws IOException {
+
+ System.out.println("Reading matches...");
+ ClassMatches classMatches = MatchesReader.readClasses(classMatchesFile);
+ MemberMatches fieldMatches = MatchesReader.readMembers(fieldMatchesFile);
+ MemberMatches methodMatches = MatchesReader.readMembers(methodMatchesFile);
+
+ Deobfuscators deobfuscators = new Deobfuscators(sourceJar, destJar);
+ deobfuscators.source.setMappings(mappings);
+
+ // apply matches
+ Mappings newMappings = MappingsConverter.newMappings(classMatches, mappings, deobfuscators.source, deobfuscators.dest);
+ MappingsConverter.applyMemberMatches(newMappings, classMatches, fieldMatches, MappingsConverter.getFieldDoer());
+ MappingsConverter.applyMemberMatches(newMappings, classMatches, methodMatches, MappingsConverter.getMethodDoer());
+
+ // check the final mappings
+ MappingsChecker checker = new MappingsChecker(deobfuscators.dest.getJarIndex());
+ checker.dropBrokenMappings(newMappings);
+
+ for (java.util.Map.Entry mapping : checker.getDroppedClassMappings().entrySet()) {
+ System.out.println("WARNING: Broken class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ")");
+ }
+ for (java.util.Map.Entry mapping : checker.getDroppedInnerClassMappings().entrySet()) {
+ System.out.println("WARNING: Broken inner class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ")");
+ }
+ for (java.util.Map.Entry mapping : checker.getDroppedFieldMappings().entrySet()) {
+ System.out.println("WARNING: Broken field entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ")");
+ }
+ for (java.util.Map.Entry mapping : checker.getDroppedMethodMappings().entrySet()) {
+ System.out.println("WARNING: Broken behavior entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ")");
+ }
+
+ //TODO Fix
+ // write out the converted mappings
+// try (FileWriter out = new FileWriter(outMappingsFile)) {
+// new MappingsWriter().write(out, newMappings);
+// }
+ System.out.println("Wrote converted mappings to:\n\t" + outMappingsFile.getAbsolutePath());
+ }
+
+ private static class Deobfuscators {
+
+ public Deobfuscator source;
+ public Deobfuscator dest;
+
+ public Deobfuscators(JarFile sourceJar, JarFile destJar) {
+ System.out.println("Indexing source jar...");
+ IndexerThread sourceIndexer = new IndexerThread(sourceJar);
+ sourceIndexer.start();
+ System.out.println("Indexing dest jar...");
+ IndexerThread destIndexer = new IndexerThread(destJar);
+ destIndexer.start();
+ sourceIndexer.joinOrBail();
+ destIndexer.joinOrBail();
+ source = sourceIndexer.deobfuscator;
+ dest = destIndexer.deobfuscator;
+ }
+ }
+
+ private static class IndexerThread extends Thread {
+
+ private JarFile m_jarFile;
+ public Deobfuscator deobfuscator;
+
+ public IndexerThread(JarFile jarFile) {
+ m_jarFile = jarFile;
+ deobfuscator = null;
+ }
+
+ public void joinOrBail() {
+ try {
+ join();
+ } catch (InterruptedException ex) {
+ throw new Error(ex);
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ deobfuscator = new Deobfuscator(m_jarFile);
+ } catch (IOException ex) {
+ throw new Error(ex);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/cuchaz/enigma/Deobfuscator.java b/src/main/java/cuchaz/enigma/Deobfuscator.java
new file mode 100644
index 00000000..2a18e657
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/Deobfuscator.java
@@ -0,0 +1,530 @@
+/*******************************************************************************
+ * 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;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import com.strobel.assembler.metadata.MetadataSystem;
+import com.strobel.assembler.metadata.TypeDefinition;
+import com.strobel.assembler.metadata.TypeReference;
+import com.strobel.decompiler.DecompilerContext;
+import com.strobel.decompiler.DecompilerSettings;
+import com.strobel.decompiler.PlainTextOutput;
+import com.strobel.decompiler.languages.java.JavaOutputVisitor;
+import com.strobel.decompiler.languages.java.ast.AstBuilder;
+import com.strobel.decompiler.languages.java.ast.CompilationUnit;
+import com.strobel.decompiler.languages.java.ast.InsertParenthesesVisitor;
+
+import java.io.*;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+
+import cuchaz.enigma.analysis.*;
+import cuchaz.enigma.bytecode.ClassProtectifier;
+import cuchaz.enigma.bytecode.ClassPublifier;
+import cuchaz.enigma.mapping.*;
+import javassist.CtClass;
+import javassist.bytecode.Descriptor;
+
+public class Deobfuscator {
+
+ public interface ProgressListener {
+ void init(int totalWork, String title);
+
+ void onProgress(int numDone, String message);
+ }
+
+ private JarFile m_jar;
+ private DecompilerSettings m_settings;
+ private JarIndex m_jarIndex;
+ private Mappings m_mappings;
+ private MappingsRenamer m_renamer;
+ private Map m_translatorCache;
+
+ public Deobfuscator(JarFile jar) throws IOException {
+ m_jar = jar;
+
+ // build the jar index
+ m_jarIndex = new JarIndex();
+ m_jarIndex.indexJar(m_jar, true);
+
+ // config the decompiler
+ m_settings = DecompilerSettings.javaDefaults();
+ m_settings.setMergeVariables(true);
+ m_settings.setForceExplicitImports(true);
+ m_settings.setForceExplicitTypeArguments(true);
+ m_settings.setShowDebugLineNumbers(true);
+ // DEBUG
+ //m_settings.setShowSyntheticMembers(true);
+
+ // init defaults
+ m_translatorCache = Maps.newTreeMap();
+
+ // init mappings
+ setMappings(new Mappings());
+ }
+
+ public JarFile getJar() {
+ return m_jar;
+ }
+
+ public String getJarName() {
+ return m_jar.getName();
+ }
+
+ public JarIndex getJarIndex() {
+ return m_jarIndex;
+ }
+
+ public Mappings getMappings() {
+ return m_mappings;
+ }
+
+ public void setMappings(Mappings val) {
+ setMappings(val, true);
+ }
+
+ public void setMappings(Mappings val, boolean warnAboutDrops) {
+ if (val == null) {
+ val = new Mappings();
+ }
+
+ // drop mappings that don't match the jar
+ MappingsChecker checker = new MappingsChecker(m_jarIndex);
+ checker.dropBrokenMappings(val);
+ if (warnAboutDrops) {
+ for (java.util.Map.Entry mapping : checker.getDroppedClassMappings().entrySet()) {
+ System.out.println("WARNING: Couldn't find class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
+ }
+ for (java.util.Map.Entry mapping : checker.getDroppedInnerClassMappings().entrySet()) {
+ System.out.println("WARNING: Couldn't find inner class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
+ }
+ for (java.util.Map.Entry mapping : checker.getDroppedFieldMappings().entrySet()) {
+ System.out.println("WARNING: Couldn't find field entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
+ }
+ for (java.util.Map.Entry mapping : checker.getDroppedMethodMappings().entrySet()) {
+ System.out.println("WARNING: Couldn't find behavior entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped.");
+ }
+ }
+
+ // check for related method inconsistencies
+ if (checker.getRelatedMethodChecker().hasProblems()) {
+ throw new Error("Related methods are inconsistent! Need to fix the mappings manually.\n" + checker.getRelatedMethodChecker().getReport());
+ }
+
+ m_mappings = val;
+ m_renamer = new MappingsRenamer(m_jarIndex, val);
+ m_translatorCache.clear();
+ }
+
+ public Translator getTranslator(TranslationDirection direction) {
+ Translator translator = m_translatorCache.get(direction);
+ if (translator == null) {
+ translator = m_mappings.getTranslator(direction, m_jarIndex.getTranslationIndex());
+ m_translatorCache.put(direction, translator);
+ }
+ return translator;
+ }
+
+ public void getSeparatedClasses(List obfClasses, List deobfClasses) {
+ for (ClassEntry obfClassEntry : m_jarIndex.getObfClassEntries()) {
+ // skip inner classes
+ if (obfClassEntry.isInnerClass()) {
+ continue;
+ }
+
+ // separate the classes
+ ClassEntry deobfClassEntry = deobfuscateEntry(obfClassEntry);
+ if (!deobfClassEntry.equals(obfClassEntry)) {
+ // if the class has a mapping, clearly it's deobfuscated
+ deobfClasses.add(deobfClassEntry);
+ } else if (!obfClassEntry.getPackageName().equals(Constants.NonePackage)) {
+ // also call it deobufscated if it's not in the none package
+ deobfClasses.add(obfClassEntry);
+ } else {
+ // otherwise, assume it's still obfuscated
+ obfClasses.add(obfClassEntry);
+ }
+ }
+ }
+
+ public CompilationUnit getSourceTree(String className) {
+
+ // we don't know if this class name is obfuscated or deobfuscated
+ // we need to tell the decompiler the deobfuscated name so it doesn't get freaked out
+ // the decompiler only sees classes after deobfuscation, so we need to load it by the deobfuscated name if there is one
+
+ // first, assume class name is deobf
+ String deobfClassName = className;
+
+ // if it wasn't actually deobf, then we can find a mapping for it and get the deobf name
+ ClassMapping classMapping = m_mappings.getClassByObf(className);
+ if (classMapping != null && classMapping.getDeobfName() != null) {
+ deobfClassName = classMapping.getDeobfName();
+ }
+
+ // set the type loader
+ TranslatingTypeLoader loader = new TranslatingTypeLoader(
+ m_jar,
+ m_jarIndex,
+ getTranslator(TranslationDirection.Obfuscating),
+ getTranslator(TranslationDirection.Deobfuscating)
+ );
+ m_settings.setTypeLoader(loader);
+
+ // see if procyon can find the type
+ TypeReference type = new MetadataSystem(loader).lookupType(deobfClassName);
+ if (type == null) {
+ throw new Error(String.format("Unable to find type: %s (deobf: %s)\nTried class names: %s",
+ className, deobfClassName, loader.getClassNamesToTry(deobfClassName)
+ ));
+ }
+ TypeDefinition resolvedType = type.resolve();
+
+ // decompile it!
+ DecompilerContext context = new DecompilerContext();
+ context.setCurrentType(resolvedType);
+ context.setSettings(m_settings);
+ AstBuilder builder = new AstBuilder(context);
+ builder.addType(resolvedType);
+ builder.runTransformations(null);
+ return builder.getCompilationUnit();
+ }
+
+ public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source) {
+ return getSourceIndex(sourceTree, source, null);
+ }
+
+ public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source, Boolean ignoreBadTokens) {
+
+ // build the source index
+ SourceIndex index;
+ if (ignoreBadTokens != null) {
+ index = new SourceIndex(source, ignoreBadTokens);
+ } else {
+ index = new SourceIndex(source);
+ }
+ sourceTree.acceptVisitor(new SourceIndexVisitor(), index);
+
+ // DEBUG
+ // sourceTree.acceptVisitor( new TreeDumpVisitor( new File( "tree.txt" ) ), null );
+
+ // resolve all the classes in the source references
+ for (Token token : index.referenceTokens()) {
+ EntryReference deobfReference = index.getDeobfReference(token);
+
+ // get the obfuscated entry
+ Entry obfEntry = obfuscateEntry(deobfReference.entry);
+
+ // try to resolve the class
+ ClassEntry resolvedObfClassEntry = m_jarIndex.getTranslationIndex().resolveEntryClass(obfEntry);
+ if (resolvedObfClassEntry != null && !resolvedObfClassEntry.equals(obfEntry.getClassEntry())) {
+ // change the class of the entry
+ obfEntry = obfEntry.cloneToNewClass(resolvedObfClassEntry);
+
+ // save the new deobfuscated reference
+ deobfReference.entry = deobfuscateEntry(obfEntry);
+ index.replaceDeobfReference(token, deobfReference);
+ }
+
+ // DEBUG
+ // System.out.println( token + " -> " + reference + " -> " + index.getReferenceToken( reference ) );
+ }
+
+ return index;
+ }
+
+ public String getSource(CompilationUnit sourceTree) {
+ // render the AST into source
+ StringWriter buf = new StringWriter();
+ sourceTree.acceptVisitor(new InsertParenthesesVisitor(), null);
+ sourceTree.acceptVisitor(new JavaOutputVisitor(new PlainTextOutput(buf), m_settings), null);
+ return buf.toString();
+ }
+
+ public void writeSources(File dirOut, ProgressListener progress) throws IOException {
+ // get the classes to decompile
+ Set classEntries = Sets.newHashSet();
+ for (ClassEntry obfClassEntry : m_jarIndex.getObfClassEntries()) {
+ // skip inner classes
+ if (obfClassEntry.isInnerClass()) {
+ continue;
+ }
+
+ classEntries.add(obfClassEntry);
+ }
+
+ if (progress != null) {
+ progress.init(classEntries.size(), "Decompiling classes...");
+ }
+
+ // DEOBFUSCATE ALL THE THINGS!! @_@
+ int i = 0;
+ for (ClassEntry obfClassEntry : classEntries) {
+ ClassEntry deobfClassEntry = deobfuscateEntry(new ClassEntry(obfClassEntry));
+ if (progress != null) {
+ progress.onProgress(i++, deobfClassEntry.toString());
+ }
+
+ try {
+ // get the source
+ String source = getSource(getSourceTree(obfClassEntry.getName()));
+
+ // write the file
+ File file = new File(dirOut, deobfClassEntry.getName().replace('.', '/') + ".java");
+ file.getParentFile().mkdirs();
+ try (FileWriter out = new FileWriter(file)) {
+ out.write(source);
+ }
+ } catch (Throwable t) {
+ // don't crash the whole world here, just log the error and keep going
+ // TODO: set up logback via log4j
+ System.err.println("Unable to deobfuscate class " + deobfClassEntry.toString() + " (" + obfClassEntry.toString() + ")");
+ t.printStackTrace(System.err);
+ }
+ }
+ if (progress != null) {
+ progress.onProgress(i, "Done!");
+ }
+ }
+
+ public void writeJar(File out, ProgressListener progress) {
+ final TranslatingTypeLoader loader = new TranslatingTypeLoader(
+ m_jar,
+ m_jarIndex,
+ getTranslator(TranslationDirection.Obfuscating),
+ getTranslator(TranslationDirection.Deobfuscating)
+ );
+ transformJar(out, progress, new ClassTransformer() {
+
+ @Override
+ public CtClass transform(CtClass c) throws Exception {
+ return loader.transformClass(c);
+ }
+ });
+ }
+
+ public void protectifyJar(File out, ProgressListener progress) {
+ transformJar(out, progress, new ClassTransformer() {
+
+ @Override
+ public CtClass transform(CtClass c) throws Exception {
+ return ClassProtectifier.protectify(c);
+ }
+ });
+ }
+
+ public void publifyJar(File out, ProgressListener progress) {
+ transformJar(out, progress, new ClassTransformer() {
+
+ @Override
+ public CtClass transform(CtClass c) throws Exception {
+ return ClassPublifier.publify(c);
+ }
+ });
+ }
+
+ private interface ClassTransformer {
+ CtClass transform(CtClass c) throws Exception;
+ }
+
+ private void transformJar(File out, ProgressListener progress, ClassTransformer transformer) {
+ try (JarOutputStream outJar = new JarOutputStream(new FileOutputStream(out))) {
+ if (progress != null) {
+ progress.init(JarClassIterator.getClassEntries(m_jar).size(), "Transforming classes...");
+ }
+
+ int i = 0;
+ for (CtClass c : JarClassIterator.classes(m_jar)) {
+ if (progress != null) {
+ progress.onProgress(i++, c.getName());
+ }
+
+ try {
+ c = transformer.transform(c);
+ outJar.putNextEntry(new JarEntry(c.getName().replace('.', '/') + ".class"));
+ outJar.write(c.toBytecode());
+ outJar.closeEntry();
+ } catch (Throwable t) {
+ throw new Error("Unable to transform class " + c.getName(), t);
+ }
+ }
+ if (progress != null) {
+ progress.onProgress(i, "Done!");
+ }
+
+ outJar.close();
+ } catch (IOException ex) {
+ throw new Error("Unable to write to Jar file!");
+ }
+ }
+
+ public T obfuscateEntry(T deobfEntry) {
+ if (deobfEntry == null) {
+ return null;
+ }
+ return getTranslator(TranslationDirection.Obfuscating).translateEntry(deobfEntry);
+ }
+
+ public T deobfuscateEntry(T obfEntry) {
+ if (obfEntry == null) {
+ return null;
+ }
+ return getTranslator(TranslationDirection.Deobfuscating).translateEntry(obfEntry);
+ }
+
+ public EntryReference obfuscateReference(EntryReference deobfReference) {
+ if (deobfReference == null) {
+ return null;
+ }
+ return new EntryReference(
+ obfuscateEntry(deobfReference.entry),
+ obfuscateEntry(deobfReference.context),
+ deobfReference
+ );
+ }
+
+ public EntryReference deobfuscateReference(EntryReference obfReference) {
+ if (obfReference == null) {
+ return null;
+ }
+ return new EntryReference(
+ deobfuscateEntry(obfReference.entry),
+ deobfuscateEntry(obfReference.context),
+ obfReference
+ );
+ }
+
+ public boolean isObfuscatedIdentifier(Entry obfEntry) {
+
+ if (obfEntry instanceof MethodEntry) {
+
+ // HACKHACK: Object methods are not obfuscated identifiers
+ MethodEntry obfMethodEntry = (MethodEntry) obfEntry;
+ String name = obfMethodEntry.getName();
+ String sig = obfMethodEntry.getSignature().toString();
+ if (name.equals("clone") && sig.equals("()Ljava/lang/Object;")) {
+ return false;
+ } else if (name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) {
+ return false;
+ } else if (name.equals("finalize") && sig.equals("()V")) {
+ return false;
+ } else if (name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) {
+ return false;
+ } else if (name.equals("hashCode") && sig.equals("()I")) {
+ return false;
+ } else if (name.equals("notify") && sig.equals("()V")) {
+ return false;
+ } else if (name.equals("notifyAll") && sig.equals("()V")) {
+ return false;
+ } else if (name.equals("toString") && sig.equals("()Ljava/lang/String;")) {
+ return false;
+ } else if (name.equals("wait") && sig.equals("()V")) {
+ return false;
+ } else if (name.equals("wait") && sig.equals("(J)V")) {
+ return false;
+ } else if (name.equals("wait") && sig.equals("(JI)V")) {
+ return false;
+ }
+ }
+
+ return m_jarIndex.containsObfEntry(obfEntry);
+ }
+
+ public boolean isRenameable(EntryReference obfReference) {
+ return obfReference.isNamed() && isObfuscatedIdentifier(obfReference.getNameableEntry());
+ }
+
+ // NOTE: these methods are a bit messy... oh well
+
+ public boolean hasDeobfuscatedName(Entry obfEntry) {
+ Translator translator = getTranslator(TranslationDirection.Deobfuscating);
+ if (obfEntry instanceof ClassEntry) {
+ ClassEntry obfClass = (ClassEntry) obfEntry;
+ List mappingChain = m_mappings.getClassMappingChain(obfClass);
+ ClassMapping classMapping = mappingChain.get(mappingChain.size() - 1);
+ return classMapping != null && classMapping.getDeobfName() != null;
+ } else if (obfEntry instanceof FieldEntry) {
+ return translator.translate((FieldEntry) obfEntry) != null;
+ } else if (obfEntry instanceof MethodEntry) {
+ return translator.translate((MethodEntry) obfEntry) != null;
+ } else if (obfEntry instanceof ConstructorEntry) {
+ // constructors have no names
+ return false;
+ } else if (obfEntry instanceof ArgumentEntry) {
+ return translator.translate((ArgumentEntry) obfEntry) != null;
+ } else {
+ throw new Error("Unknown entry type: " + obfEntry.getClass().getName());
+ }
+ }
+
+ public void rename(Entry obfEntry, String newName) {
+ if (obfEntry instanceof ClassEntry) {
+ m_renamer.setClassName((ClassEntry) obfEntry, Descriptor.toJvmName(newName));
+ } else if (obfEntry instanceof FieldEntry) {
+ m_renamer.setFieldName((FieldEntry) obfEntry, newName);
+ } else if (obfEntry instanceof MethodEntry) {
+ m_renamer.setMethodTreeName((MethodEntry) obfEntry, newName);
+ } else if (obfEntry instanceof ConstructorEntry) {
+ throw new IllegalArgumentException("Cannot rename constructors");
+ } else if (obfEntry instanceof ArgumentEntry) {
+ m_renamer.setArgumentName((ArgumentEntry) obfEntry, newName);
+ } else {
+ throw new Error("Unknown entry type: " + obfEntry.getClass().getName());
+ }
+
+ // clear caches
+ m_translatorCache.clear();
+ }
+
+ public void removeMapping(Entry obfEntry) {
+ if (obfEntry instanceof ClassEntry) {
+ m_renamer.removeClassMapping((ClassEntry) obfEntry);
+ } else if (obfEntry instanceof FieldEntry) {
+ m_renamer.removeFieldMapping((FieldEntry) obfEntry);
+ } else if (obfEntry instanceof MethodEntry) {
+ m_renamer.removeMethodTreeMapping((MethodEntry) obfEntry);
+ } else if (obfEntry instanceof ConstructorEntry) {
+ throw new IllegalArgumentException("Cannot rename constructors");
+ } else if (obfEntry instanceof ArgumentEntry) {
+ m_renamer.removeArgumentMapping((ArgumentEntry) obfEntry);
+ } else {
+ throw new Error("Unknown entry type: " + obfEntry);
+ }
+
+ // clear caches
+ m_translatorCache.clear();
+ }
+
+ public void markAsDeobfuscated(Entry obfEntry) {
+ if (obfEntry instanceof ClassEntry) {
+ m_renamer.markClassAsDeobfuscated((ClassEntry) obfEntry);
+ } else if (obfEntry instanceof FieldEntry) {
+ m_renamer.markFieldAsDeobfuscated((FieldEntry) obfEntry);
+ } else if (obfEntry instanceof MethodEntry) {
+ m_renamer.markMethodTreeAsDeobfuscated((MethodEntry) obfEntry);
+ } else if (obfEntry instanceof ConstructorEntry) {
+ throw new IllegalArgumentException("Cannot rename constructors");
+ } else if (obfEntry instanceof ArgumentEntry) {
+ m_renamer.markArgumentAsDeobfuscated((ArgumentEntry) obfEntry);
+ } else {
+ throw new Error("Unknown entry type: " + obfEntry);
+ }
+
+ // clear caches
+ m_translatorCache.clear();
+ }
+}
diff --git a/src/main/java/cuchaz/enigma/ExceptionIgnorer.java b/src/main/java/cuchaz/enigma/ExceptionIgnorer.java
new file mode 100644
index 00000000..fc89fa6d
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/ExceptionIgnorer.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;
+
+public class ExceptionIgnorer {
+
+ public static boolean shouldIgnore(Throwable t) {
+
+ // is this that pesky concurrent access bug in the highlight painter system?
+ // (ancient ui code is ancient)
+ if (t instanceof ArrayIndexOutOfBoundsException) {
+ StackTraceElement[] stackTrace = t.getStackTrace();
+ if (stackTrace.length > 1) {
+
+ // does this stack frame match javax.swing.text.DefaultHighlighter.paint*() ?
+ StackTraceElement frame = stackTrace[1];
+ if (frame.getClassName().equals("javax.swing.text.DefaultHighlighter") && frame.getMethodName().startsWith("paint")) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/src/main/java/cuchaz/enigma/Main.java b/src/main/java/cuchaz/enigma/Main.java
new file mode 100644
index 00000000..68959aa5
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/Main.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * 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;
+
+import java.io.File;
+import java.util.jar.JarFile;
+
+import cuchaz.enigma.gui.Gui;
+
+public class Main {
+
+ public static void main(String[] args) throws Exception {
+ Gui gui = new Gui();
+
+ // parse command-line args
+ if (args.length >= 1) {
+ gui.getController().openJar(new JarFile(getFile(args[0])));
+ }
+ if (args.length >= 2) {
+ gui.getController().openMappings(getFile(args[1]));
+ }
+
+ // DEBUG
+ //gui.getController().openDeclaration(new ClassEntry("none/bxq"));
+ }
+
+ private static File getFile(String path) {
+ // expand ~ to the home dir
+ if (path.startsWith("~")) {
+ // get the home dir
+ File dirHome = new File(System.getProperty("user.home"));
+
+ // is the path just ~/ or is it ~user/ ?
+ if (path.startsWith("~/")) {
+ return new File(dirHome, path.substring(2));
+ } else {
+ return new File(dirHome.getParentFile(), path.substring(1));
+ }
+ }
+
+ return new File(path);
+ }
+}
diff --git a/src/main/java/cuchaz/enigma/MainFormatConverter.java b/src/main/java/cuchaz/enigma/MainFormatConverter.java
new file mode 100644
index 00000000..29e334ec
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/MainFormatConverter.java
@@ -0,0 +1,121 @@
+/*******************************************************************************
+ * 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;
+
+import com.google.common.collect.Maps;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.jar.JarFile;
+
+import cuchaz.enigma.analysis.JarClassIterator;
+import cuchaz.enigma.mapping.*;
+import javassist.CtClass;
+import javassist.CtField;
+
+public class MainFormatConverter {
+
+ public static void main(String[] args)
+ throws Exception {
+
+ System.out.println("Getting field types from jar...");
+
+ JarFile jar = new JarFile(System.getProperty("user.home") + "/.minecraft/versions/1.8/1.8.jar");
+ Map fieldTypes = Maps.newHashMap();
+ for (CtClass c : JarClassIterator.classes(jar)) {
+ for (CtField field : c.getDeclaredFields()) {
+ FieldEntry fieldEntry = EntryFactory.getFieldEntry(field);
+ fieldTypes.put(getFieldKey(fieldEntry), moveClasssesOutOfDefaultPackage(fieldEntry.getType()));
+ }
+ }
+
+ System.out.println("Reading mappings...");
+
+ File fileMappings = new File("../Enigma Mappings/1.8.mappings");
+ MappingsReader mappingsReader = new MappingsReader() {
+
+ @Override
+ protected FieldMapping readField(String obf, String deobf, String type) {
+ // assume the void type for now
+ return new FieldMapping(obf, new Type("V"), deobf);
+ }
+ };
+ Mappings mappings = mappingsReader.read(fileMappings);
+
+ System.out.println("Updating field types...");
+
+ for (ClassMapping classMapping : mappings.classes()) {
+ updateFieldsInClass(fieldTypes, classMapping);
+ }
+
+ System.out.println("Saving mappings...");
+
+ //TODO Fix
+// try (FileWriter writer = new FileWriter(fileMappings)) {
+// new MappingsWriter().write(writer, mappings);
+// }
+
+ System.out.println("Done!");
+ }
+
+ private static Type moveClasssesOutOfDefaultPackage(Type type) {
+ return new Type(type, new ClassNameReplacer() {
+ @Override
+ public String replace(String className) {
+ ClassEntry entry = new ClassEntry(className);
+ if (entry.isInDefaultPackage()) {
+ return Constants.NonePackage + "/" + className;
+ }
+ return null;
+ }
+ });
+ }
+
+ private static void updateFieldsInClass(Map fieldTypes, ClassMapping classMapping)
+ throws Exception {
+
+ // update the fields
+ for (FieldMapping fieldMapping : classMapping.fields()) {
+ setFieldType(fieldTypes, classMapping, fieldMapping);
+ }
+
+ // recurse
+ for (ClassMapping innerClassMapping : classMapping.innerClasses()) {
+ updateFieldsInClass(fieldTypes, innerClassMapping);
+ }
+ }
+
+ private static void setFieldType(Map fieldTypes, ClassMapping classMapping, FieldMapping fieldMapping)
+ throws Exception {
+
+ // get the new type
+ Type newType = fieldTypes.get(getFieldKey(classMapping, fieldMapping));
+ if (newType == null) {
+ throw new Error("Can't find type for field: " + getFieldKey(classMapping, fieldMapping));
+ }
+
+ // hack in the new field type
+ Field field = fieldMapping.getClass().getDeclaredField("m_obfType");
+ field.setAccessible(true);
+ field.set(fieldMapping, newType);
+ }
+
+ private static Object getFieldKey(ClassMapping classMapping, FieldMapping fieldMapping) {
+ return classMapping.getObfSimpleName() + "." + fieldMapping.getObfName();
+ }
+
+ private static String getFieldKey(FieldEntry obfFieldEntry) {
+ return obfFieldEntry.getClassEntry().getSimpleName() + "." + obfFieldEntry.getName();
+ }
+}
diff --git a/src/main/java/cuchaz/enigma/TranslatingTypeLoader.java b/src/main/java/cuchaz/enigma/TranslatingTypeLoader.java
new file mode 100644
index 00000000..6c429a6a
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/TranslatingTypeLoader.java
@@ -0,0 +1,241 @@
+/*******************************************************************************
+ * 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;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import com.strobel.assembler.metadata.Buffer;
+import com.strobel.assembler.metadata.ClasspathTypeLoader;
+import com.strobel.assembler.metadata.ITypeLoader;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import cuchaz.enigma.analysis.BridgeMarker;
+import cuchaz.enigma.analysis.JarIndex;
+import cuchaz.enigma.bytecode.*;
+import cuchaz.enigma.mapping.ClassEntry;
+import cuchaz.enigma.mapping.Translator;
+import javassist.*;
+import javassist.bytecode.Descriptor;
+
+public class TranslatingTypeLoader implements ITypeLoader {
+
+ private JarFile m_jar;
+ private JarIndex m_jarIndex;
+ private Translator m_obfuscatingTranslator;
+ private Translator m_deobfuscatingTranslator;
+ private Map m_cache;
+ private ClasspathTypeLoader m_defaultTypeLoader;
+
+ public TranslatingTypeLoader(JarFile jar, JarIndex jarIndex) {
+ this(jar, jarIndex, new Translator(), new Translator());
+ }
+
+ public TranslatingTypeLoader(JarFile jar, JarIndex jarIndex, Translator obfuscatingTranslator, Translator deobfuscatingTranslator) {
+ m_jar = jar;
+ m_jarIndex = jarIndex;
+ m_obfuscatingTranslator = obfuscatingTranslator;
+ m_deobfuscatingTranslator = deobfuscatingTranslator;
+ m_cache = Maps.newHashMap();
+ m_defaultTypeLoader = new ClasspathTypeLoader();
+ }
+
+ public void clearCache() {
+ m_cache.clear();
+ }
+
+ @Override
+ public boolean tryLoadType(String className, Buffer out) {
+
+ // check the cache
+ byte[] data;
+ if (m_cache.containsKey(className)) {
+ data = m_cache.get(className);
+ } else {
+ data = loadType(className);
+ m_cache.put(className, data);
+ }
+
+ if (data == null) {
+ // chain to default type loader
+ return m_defaultTypeLoader.tryLoadType(className, out);
+ }
+
+ // send the class to the decompiler
+ out.reset(data.length);
+ System.arraycopy(data, 0, out.array(), out.position(), data.length);
+ out.position(0);
+ return true;
+ }
+
+ public CtClass loadClass(String deobfClassName) {
+
+ byte[] data = loadType(deobfClassName);
+ if (data == null) {
+ return null;
+ }
+
+ // return a javassist handle for the class
+ String javaClassFileName = Descriptor.toJavaName(deobfClassName);
+ ClassPool classPool = new ClassPool();
+ classPool.insertClassPath(new ByteArrayClassPath(javaClassFileName, data));
+ try {
+ return classPool.get(javaClassFileName);
+ } catch (NotFoundException ex) {
+ throw new Error(ex);
+ }
+ }
+
+ private byte[] loadType(String className) {
+
+ // NOTE: don't know if class name is obf or deobf
+ ClassEntry classEntry = new ClassEntry(className);
+ ClassEntry obfClassEntry = m_obfuscatingTranslator.translateEntry(classEntry);
+
+ // is this an inner class referenced directly? (ie trying to load b instead of a$b)
+ if (!obfClassEntry.isInnerClass()) {
+ List classChain = m_jarIndex.getObfClassChain(obfClassEntry);
+ if (classChain.size() > 1) {
+ System.err.println(String.format("WARNING: no class %s after inner class reconstruction. Try %s",
+ className, obfClassEntry.buildClassEntry(classChain)
+ ));
+ return null;
+ }
+ }
+
+ // is this a class we should even know about?
+ if (!m_jarIndex.containsObfClass(obfClassEntry)) {
+ return null;
+ }
+
+ // DEBUG
+ //System.out.println(String.format("Looking for %s (obf: %s)", classEntry.getName(), obfClassEntry.getName()));
+
+ // find the class in the jar
+ String classInJarName = findClassInJar(obfClassEntry);
+ if (classInJarName == null) {
+ // couldn't find it
+ return null;
+ }
+
+ try {
+ // read the class file into a buffer
+ ByteArrayOutputStream data = new ByteArrayOutputStream();
+ byte[] buf = new byte[1024 * 1024]; // 1 KiB
+ InputStream in = m_jar.getInputStream(m_jar.getJarEntry(classInJarName + ".class"));
+ while (true) {
+ int bytesRead = in.read(buf);
+ if (bytesRead <= 0) {
+ break;
+ }
+ data.write(buf, 0, bytesRead);
+ }
+ data.close();
+ in.close();
+ buf = data.toByteArray();
+
+ // load the javassist handle to the raw class
+ ClassPool classPool = new ClassPool();
+ String classInJarJavaName = Descriptor.toJavaName(classInJarName);
+ classPool.insertClassPath(new ByteArrayClassPath(classInJarJavaName, buf));
+ CtClass c = classPool.get(classInJarJavaName);
+
+ c = transformClass(c);
+
+ // sanity checking
+ assertClassName(c, classEntry);
+
+ // DEBUG
+ //Util.writeClass( c );
+
+ // we have a transformed class!
+ return c.toBytecode();
+ } catch (IOException | NotFoundException | CannotCompileException ex) {
+ throw new Error(ex);
+ }
+ }
+
+ private String findClassInJar(ClassEntry obfClassEntry) {
+
+ // try to find the class in the jar
+ for (String className : getClassNamesToTry(obfClassEntry)) {
+ JarEntry jarEntry = m_jar.getJarEntry(className + ".class");
+ if (jarEntry != null) {
+ return className;
+ }
+ }
+
+ // didn't find it ;_;
+ return null;
+ }
+
+ public List getClassNamesToTry(String className) {
+ return getClassNamesToTry(m_obfuscatingTranslator.translateEntry(new ClassEntry(className)));
+ }
+
+ public List getClassNamesToTry(ClassEntry obfClassEntry) {
+ List classNamesToTry = Lists.newArrayList();
+ classNamesToTry.add(obfClassEntry.getName());
+ if (obfClassEntry.getPackageName().equals(Constants.NonePackage)) {
+ // taking off the none package, if any
+ classNamesToTry.add(obfClassEntry.getSimpleName());
+ }
+ if (obfClassEntry.isInnerClass()) {
+ // try just the inner class name
+ classNamesToTry.add(obfClassEntry.getInnermostClassName());
+ }
+ return classNamesToTry;
+ }
+
+ public CtClass transformClass(CtClass c)
+ throws IOException, NotFoundException, CannotCompileException {
+
+ // we moved a lot of classes out of the default package into the none package
+ // make sure all the class references are consistent
+ ClassRenamer.moveAllClassesOutOfDefaultPackage(c, Constants.NonePackage);
+
+ // reconstruct inner classes
+ new InnerClassWriter(m_jarIndex).write(c);
+
+ // re-get the javassist handle since we changed class names
+ ClassEntry obfClassEntry = new ClassEntry(Descriptor.toJvmName(c.getName()));
+ String javaClassReconstructedName = Descriptor.toJavaName(obfClassEntry.getName());
+ ClassPool classPool = new ClassPool();
+ classPool.insertClassPath(new ByteArrayClassPath(javaClassReconstructedName, c.toBytecode()));
+ c = classPool.get(javaClassReconstructedName);
+
+ // check that the file is correct after inner class reconstruction (ie cause Javassist to fail fast if something is wrong)
+ assertClassName(c, obfClassEntry);
+
+ // do all kinds of deobfuscating transformations on the class
+ new BridgeMarker(m_jarIndex).markBridges(c);
+ new MethodParameterWriter(m_deobfuscatingTranslator).writeMethodArguments(c);
+ new LocalVariableRenamer(m_deobfuscatingTranslator).rename(c);
+ new ClassTranslator(m_deobfuscatingTranslator).translate(c);
+
+ return c;
+ }
+
+ private void assertClassName(CtClass c, ClassEntry obfClassEntry) {
+ String name1 = Descriptor.toJvmName(c.getName());
+ assert (name1.equals(obfClassEntry.getName())) : String.format("Looking for %s, instead found %s", obfClassEntry.getName(), name1);
+
+ String name2 = Descriptor.toJvmName(c.getClassFile().getName());
+ assert (name2.equals(obfClassEntry.getName())) : String.format("Looking for %s, instead found %s", obfClassEntry.getName(), name2);
+ }
+}
diff --git a/src/main/java/cuchaz/enigma/Util.java b/src/main/java/cuchaz/enigma/Util.java
new file mode 100644
index 00000000..1bcdb9ea
--- /dev/null
+++ b/src/main/java/cuchaz/enigma/Util.java
@@ -0,0 +1,99 @@
+/*******************************************************************************
+ * 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;
+
+import com.google.common.io.CharStreams;
+
+import java.awt.Desktop;
+import java.io.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.jar.JarFile;
+
+import javassist.CannotCompileException;
+import javassist.CtClass;
+import javassist.bytecode.Descriptor;
+
+public class Util {
+
+ public static int combineHashesOrdered(Object... objs) {
+ return combineHashesOrdered(Arrays.asList(objs));
+ }
+
+ public static int combineHashesOrdered(Iterable