package installer;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarOutputStream;
import java.util.jar.Pack200;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import immibis.bon.com.immibis.json.JsonReader;
import lzma.sdk.lzma.Decoder;
import lzma.streams.LzmaInputStream;
import net.mcforkage.ant.ApplyDiff2Task;
import net.mcforkage.ant.MergeJarsTask;
import net.mcforkage.ant.UncompressDiff2Task;
import net.mcforkage.ant.compression.BitInputStream;
import net.mcforkage.ant.diff2.ApplyDiff2;
import net.mcforkage.ant.diff2.UncompressDiff2;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import bytecode.AddOBFID;
import bytecode.ApplyAT;
import bytecode.ApplyExceptions;
import bytecode.ApplyExceptorJson;
import bytecode.ApplyParamNames;
import bytecode.ApplySRG;
import bytecode.BaseStreamingJarProcessor;
import bytecode.Bytecode2Text;
import bytecode.JarMerger;
import bytecode.RemoveGenericMethods;
import bytecode.SortZipEntries;
import bytecode.Text2Bytecode;
import bytecode.TrimBytecode;
import bytecode.patchfile.PatchFile;
public class Installer {
public static File install(File clientJar, File serverJar, File tempDir, final Map<String, byte[]> installData, final ProgressDialog dlg) throws Exception {
File merged = new File(tempDir, "merged.jar");
File srg = new File(tempDir, "srg.jar");
File unsorted = new File(tempDir, "unsorted.jar");
File sorted = new File(tempDir, "sorted.jar");
File unpatchedBytecodeFile;
int numClassFiles = 0;
final int[] numClassFilesRef = {0};
if(Boolean.getBoolean("minecraftforkage.installer.readUnpatchedBytecodeFromFile")) {
unpatchedBytecodeFile = new File("../bytecode-orig.txt");
}
else
{
if(dlg != null) dlg.startIndeterminate("Merging JARs");
JarMerger.merge(clientJar, serverJar, merged, new InputStreamReader(new ByteArrayInputStream(installData.get("mcp_merge.cfg"))), dlg);
if(dlg != null) dlg.startIndeterminate("Applying deobfuscation mapping");
ApplySRG.apply(new InputStreamReader(new ByteArrayInputStream(installData.get("joined.srg"))), merged, srg, dlg);
final Object exceptor_json = JsonReader.readJSON(new InputStreamReader(new ByteArrayInputStream(installData.get("exceptor.json"))));
final List<ApplyAT.Pattern> fml_at = ApplyAT.loadActions(new InputStreamReader(new ByteArrayInputStream(installData.get("fml_at.cfg"))));
final List<ApplyAT.Pattern> forge_at = ApplyAT.loadActions(new InputStreamReader(new ByteArrayInputStream(installData.get("forge_at.cfg"))));
final ApplyExceptions exceptions = new ApplyExceptions();
final ApplyParamNames params = new ApplyParamNames();
final AddOBFID obfid = new AddOBFID();
final TrimBytecode trim = new TrimBytecode();
final RemoveGenericMethods removeGenericBridges = new RemoveGenericMethods();
exceptions.loadConfig(new InputStreamReader(new ByteArrayInputStream(installData.get("joined.exc"))));
params.loadConfig(new InputStreamReader(new ByteArrayInputStream(installData.get("joined.exc"))));
obfid.loadConfig(new InputStreamReader(new ByteArrayInputStream(installData.get("joined.exc"))));
if(dlg != null) dlg.startIndeterminate("Processing bytecode");
new BaseStreamingJarProcessor() {
@Override
public void loadConfig(Reader file) throws Exception {
}
@Override
public ClassVisitor createClassVisitor(ClassVisitor cv) throws Exception {
cv = trim.createClassVisitor(cv);
cv = removeGenericBridges.createClassVisitor(cv);
cv = obfid.createClassVisitor(cv);
cv = params.createClassVisitor(cv);
cv = exceptions.createClassVisitor(cv);
cv = new ApplyAT.ApplyATClassVisitor(cv, forge_at);
cv = new ApplyAT.ApplyATClassVisitor(cv, fml_at);
cv = new ApplyExceptorJson.ApplyJsonClassVisitor(cv, (Map)exceptor_json);
numClassFilesRef[0]++;
return cv;
}
}.go(new FileInputStream(srg), new FileOutputStream(unsorted));
numClassFiles = numClassFilesRef[0];
if(dlg != null) dlg.startIndeterminate("Sorting class files");
SortZipEntries.sort(unsorted, null, false, new FileOutputStream(sorted));
unpatchedBytecodeFile = new File(tempDir, "bytecode-unpatched.txt");
if(dlg != null) dlg.startIndeterminate("Converting bytecode to patchable format");
try (PrintStream fout = new PrintStream(new BufferedOutputStream(new FileOutputStream(unpatchedBytecodeFile), 262144))) {
if(dlg != null) dlg.initProgressBar(0, numClassFiles);
Bytecode2Text.go(new FileInputStream(sorted), fout, dlg);
}
}
File patchedBytecodeFile = new File(tempDir, "bytecode-patched.txt");
if(dlg != null) dlg.startIndeterminate("Applying bytecode patch");
{
try (final PipedReader patch_in = new PipedReader()) {
new Thread() {
@Override
public void run() {
try (PrintWriter patch_out = new PrintWriter(new PipedWriter(patch_in))) {
try (BitInputStream compressed_patch_in = new BitInputStream(new ByteArrayInputStream(installData.get("bytecode.patch2z")))) {
UncompressDiff2.uncompress(compressed_patch_in, patch_out);
}
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}.start();
try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(patchedBytecodeFile), StandardCharsets.UTF_8))) {
ApplyDiff2.apply(ApplyDiff2.readFile(unpatchedBytecodeFile), new BufferedReader(patch_in), out);
}
}
}
if(dlg != null) dlg.startIndeterminate("Converting back to JAR format");
ByteArrayOutputStream patchedJarBAOS = new ByteArrayOutputStream();
try (JarOutputStream patchedJarOut = new JarOutputStream(patchedJarBAOS)) {
try (BufferedReader patchedBytecodeIn = new BufferedReader(new InputStreamReader(new BufferedInputStream(new FileInputStream(patchedBytecodeFile), 262144), StandardCharsets.UTF_8))) {
if(dlg != null) dlg.initProgressBar(0, numClassFiles);
new Text2Bytecode(patchedBytecodeIn, patchedJarOut, dlg).run();
}
Pack200.newUnpacker().unpack(new ByteArrayInputStream(installData.get("new-classes.pack")), patchedJarOut);
try (ZipInputStream clientJarIn = new ZipInputStream(new FileInputStream(clientJar))) {
copyResourcesOnly(clientJarIn, patchedJarOut);
}
}
byte[] patchedJarBytes = patchedJarBAOS.toByteArray();
patchedJarBAOS = null;
if(dlg != null) dlg.startIndeterminate("Extracting superclasses");
if(dlg != null) dlg.initProgressBar(0, numClassFiles);
// find superclass of every class (not interface)
// Previous step added some class files, but we don't know how many, so re-count that too.
numClassFilesRef[0] = 0;
final Map<String, String> superclasses = new HashMap<String, String>();
try (ZipInputStream patchedJarIn = new ZipInputStream(new ByteArrayInputStream(patchedJarBytes))) {
ZipEntry ze;
while((ze = patchedJarIn.getNextEntry()) != null) {
if(ze.getName().endsWith(".class")) {
numClassFilesRef[0]++;
new ClassReader(patchedJarIn).accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if(dlg != null) dlg.incrementProgress(1);
if((access & Opcodes.ACC_INTERFACE) == 0) {
System.err.println("superclass of "+name+" is "+superName);
superclasses.put(name, superName);
}
}
}, 0);
}
patchedJarIn.closeEntry();
}
}
numClassFiles = numClassFilesRef[0];
File finalResultJar = new File(tempDir, "patched.jar");
if(dlg != null) dlg.startIndeterminate("Pre-verifying JAR");
if(dlg != null) dlg.initProgressBar(0, numClassFiles);
// compute frames and maxes for all methods; bytecode patching doesn't preserve them
try (ZipInputStream patchedJarIn = new ZipInputStream(new ByteArrayInputStream(patchedJarBytes))) {
try (ZipOutputStream completeJarOut = new ZipOutputStream(new FileOutputStream(finalResultJar))) {
ZipEntry ze;
while((ze = patchedJarIn.getNextEntry()) != null) {
completeJarOut.putNextEntry(new ZipEntry(ze.getName()));
if(ze.getName().endsWith(".class")) {
if(dlg != null) dlg.incrementProgress(1);
System.err.println("Generating frames for "+ze.getName());
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) {
@Override
protected String getCommonSuperClass(String type1, String type2) {
if(isSuperclassOf(type1, type2, superclasses)) return type1;
if(isSuperclassOf(type2, type1, superclasses)) return type2;
do {
//System.err.println("getCommonSuperClass("+type1+","+type2+")");
String next = superclasses.get(type1);
if(next == null) {
System.err.println("Don't know superclass of "+type1);
return "java/lang/Object";
}
type1 = next;
} while (type1 != null && !isSuperclassOf(type1, type2, superclasses));
return type1;
}
private boolean isSuperclassOf(String _super, String _sub, Map<String, String> superclasses) {
if(_super.equals(_sub)) return true;
if(_sub.equals("java/lang/Object")) return false;
String next = superclasses.get(_sub);
//System.err.println("isSuperclassOf("+_super+","+_sub+")");
if(next == null) {
System.err.println("Don't know superclass of "+_sub);
return false;
}
return isSuperclassOf(_super, next, superclasses);
}
};
new ClassReader(patchedJarIn).accept(cw, 0);
completeJarOut.write(cw.toByteArray());
} else {
Utils.copyStream(patchedJarIn, completeJarOut);
}
completeJarOut.closeEntry();
patchedJarIn.closeEntry();
}
}
}
return finalResultJar;
}
private static void copyResourcesOnly(ZipInputStream clientJarIn, ZipOutputStream patchedJarOut) throws Exception {
ZipEntry ze;
while((ze = clientJarIn.getNextEntry()) != null) {
if(!ze.getName().endsWith("/") && !ze.getName().endsWith(".class") && !ze.getName().startsWith("META-INF/")) {
patchedJarOut.putNextEntry(new ZipEntry(ze.getName()));
Utils.copyStream(clientJarIn, patchedJarOut);
patchedJarOut.closeEntry();
}
clientJarIn.closeEntry();
}
}
}