package bytecode;
import installer.ProgressDialog;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.*;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
public class JarMerger {
private static class Config {
Collection<String> ignore = new ArrayList<>();
public boolean isIgnored(String name) {
for(String s : ignore)
if(name.startsWith(s))
return true;
return false;
}
}
public static void main(String[] args) {
if(args.length != 4) {
System.err.println("Usage: java JarMerger <clientpath> <serverpath> <outputpath> <configpath>");
System.exit(1);
}
try {
merge(new File(args[0]), new File(args[1]), new File(args[2]), new FileReader(new File(args[3])), null);
} catch(Throwable t) {
t.printStackTrace();
System.exit(1);
}
}
// Closes `config`.
public static void merge(File client, File server, File outFile, Reader config, ProgressDialog dlg) throws IOException {
Config cfg = readConfig(config);
try (ZipFile clientJF = new JarFile(client)) {
try (ZipFile serverJF = new JarFile(server)) {
try (ZipOutputStream outJF = new ZipOutputStream(new FileOutputStream(outFile))) {
merge(clientJF, serverJF, outJF, cfg, dlg);
}
}
}
}
// Closes `stream`.
@SuppressWarnings("resource") // eclipse compiler bug causes spurious warning on `br` in try-with-resources statement
private static Config readConfig(Reader stream) throws IOException {
Config cfg = new Config();
try (BufferedReader br = new BufferedReader(stream)) {
String line;
while((line = br.readLine()) != null) {
if(line.contains("#"))
line = line.split("#")[0];
char cmd = line.charAt(0);
String arg = line.substring(1).trim();
if(cmd == '^')
cfg.ignore.add(arg);
else
throw new IOException("Unknonwn mcp_merge.cfg command character: "+cmd);
}
}
return cfg;
}
// Does not close `clientJF`, `serverJF` or `outJF`.
public static void merge(ZipFile clientJF, ZipFile serverJF, ZipOutputStream outJF, Config cfg, ProgressDialog dlg) throws IOException {
Set<String> seenResources = new TreeSet<>();
Map<String, ZipEntry> clientClasses = new TreeMap<>();
Map<String, ZipEntry> serverClasses = new TreeMap<>();
Set<String> commonClasses = new TreeSet<>();
gatherClassNamesAndCopyResources(clientJF, outJF, seenResources, clientClasses, cfg);
gatherClassNamesAndCopyResources(serverJF, outJF, seenResources, serverClasses, cfg);
commonClasses.addAll(clientClasses.keySet());
commonClasses.retainAll(serverClasses.keySet());
writeOneSidedClasses(clientJF, outJF, clientClasses, serverClasses.keySet(), "CLIENT");
writeOneSidedClasses(serverJF, outJF, serverClasses, clientClasses.keySet(), "SERVER");
writeMergedClasses(clientJF, serverJF, outJF, commonClasses, dlg);
}
private static void writeOneSidedClasses(ZipFile inJF, ZipOutputStream outJF, Map<String, ZipEntry> classes, Set<String> otherSideClasses, final String sideAnnotation) throws IOException {
for(Map.Entry<String, ZipEntry> entry : classes.entrySet()) {
if(otherSideClasses.contains(entry.getKey()))
continue;
byte[] data;
try (InputStream entryIn = inJF.getInputStream(entry.getValue())) {
ClassReader cr = new ClassReader(entryIn);
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ClassVisitor(Opcodes.ASM4, cw) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
AnnotationVisitor av = super.visitAnnotation("Lcpw/mods/fml/relauncher/SideOnly;", true);
if(av != null) {
av.visitEnum("value", "Lcpw/mods/fml/relauncher/Side;", sideAnnotation);
av.visitEnd();
}
}
}, 0);
data = cw.toByteArray();
}
outJF.putNextEntry(new ZipEntry(entry.getValue().getName()));
outJF.write(data);
outJF.closeEntry();
}
}
private static void writeMergedClasses(ZipFile clientJF, ZipFile serverJF, ZipOutputStream outJF, Set<String> commonClasses, ProgressDialog dlg) throws IOException {
if(dlg != null) dlg.initProgressBar(0, commonClasses.size());
for(String classname : commonClasses) {
String filename = classname.replace('.', '/') + ".class";
ClassNode clientCN = new ClassNode();
ClassNode serverCN = new ClassNode();
try (InputStream entryIn = clientJF.getInputStream(clientJF.getEntry(filename))) {
new ClassReader(entryIn).accept(clientCN, 0);
}
try (InputStream entryIn = serverJF.getInputStream(serverJF.getEntry(filename))) {
new ClassReader(entryIn).accept(serverCN, 0);
}
mergeClasses(clientCN, serverCN);
ClassWriter cw = new ClassWriter(0);
clientCN.accept(cw);
outJF.putNextEntry(new ZipEntry(filename));
outJF.write(cw.toByteArray());
outJF.closeEntry();
if(dlg != null) dlg.incrementProgress(1);
}
}
private static void gatherClassNamesAndCopyResources(ZipFile inJF, ZipOutputStream outJF, Set<String> seenResources, Map<String, ZipEntry> thisSideClasses, Config cfg) throws IOException {
byte[] buffer = new byte[32768];
Enumeration<? extends ZipEntry> entries = inJF.entries();
while(entries.hasMoreElements()) {
ZipEntry ze = entries.nextElement();
if(ze.getName().endsWith(".class")) {
if(!cfg.isIgnored(ze.getName())) {
String className = ze.getName().substring(0, ze.getName().length() - 6).replace('/', '.');
thisSideClasses.put(className, ze);
}
} else if(seenResources.add(ze.getName()) && !ze.getName().equals("META-INF/MANIFEST.MF")) {
if(!cfg.isIgnored(ze.getName())) {
outJF.putNextEntry(new ZipEntry(ze.getName()));
try (InputStream in = inJF.getInputStream(ze)) {
while(true) {
int read = in.read(buffer);
if(read <= 0)
break;
outJF.write(buffer, 0, read);
}
}
outJF.closeEntry();
}
}
}
}
private static AnnotationNode getAnnotationNode(String side) {
AnnotationNode an = new AnnotationNode("Lcpw/mods/fml/relauncher/SideOnly;");
an.values = Arrays.<Object>asList("value", new String[] {"Lcpw/mods/fml/relauncher/Side;", side});
return an;
}
private static void addAnnotationNode(FieldNode fn, String side) {
if(fn.visibleAnnotations == null)
fn.visibleAnnotations = new ArrayList<>(1);
fn.visibleAnnotations.add(getAnnotationNode(side));
}
private static void addAnnotationNode(MethodNode mn, String side) {
if(mn.visibleAnnotations == null)
mn.visibleAnnotations = new ArrayList<>(1);
mn.visibleAnnotations.add(getAnnotationNode(side));
}
// Merge server-only fields and methods into client. Add SideOnly annotations to fields and methods.
// On exit, `client` holds the result and `server` is trashed.
private static void mergeClasses(ClassNode client, ClassNode server) {
mergeFields(client, server);
mergeMethods(client, server);
}
private static void mergeFields(ClassNode client, ClassNode server) {
Map<String, FieldNode> clientFields = new TreeMap<>();
Map<String, FieldNode> serverFields = new TreeMap<>();
Set<String> fieldNames = new TreeSet<>();
for(FieldNode fn : client.fields) {clientFields.put(fn.name, fn); fieldNames.add(fn.name);}
for(FieldNode fn : server.fields) {serverFields.put(fn.name, fn); fieldNames.add(fn.name);}
for(String fname : fieldNames) {
FieldNode cl = clientFields.get(fname);
FieldNode sv = serverFields.get(fname);
if(cl != null && sv != null)
continue; // nothing to do
if(cl != null) {
addAnnotationNode(cl, "CLIENT");
} else if(sv != null) {
addAnnotationNode(sv, "SERVER");
client.fields.add(sv);
} else
throw new AssertionError("shouldn't get here");
}
}
private static void mergeMethods(ClassNode client, ClassNode server) {
Map<String, MethodNode> clientMethods = new TreeMap<>();
Map<String, MethodNode> serverMethods = new TreeMap<>();
Set<String> methods = new TreeSet<>();
for(MethodNode fn : client.methods) {clientMethods.put(fn.name+fn.desc, fn); methods.add(fn.name+fn.desc);}
for(MethodNode fn : server.methods) {serverMethods.put(fn.name+fn.desc, fn); methods.add(fn.name+fn.desc);}
for(String fname : methods) {
MethodNode cl = clientMethods.get(fname);
MethodNode sv = serverMethods.get(fname);
if(cl != null && sv != null)
continue; // nothing to do
if(cl != null) {
addAnnotationNode(cl, "CLIENT");
} else if(sv != null) {
addAnnotationNode(sv, "SERVER");
client.methods.add(sv);
} else
throw new AssertionError("shouldn't get here");
}
}
}