001 package cpw.mods.fml.common.asm.transformers;
002
003 import java.io.BufferedOutputStream;
004 import java.io.BufferedReader;
005 import java.io.ByteArrayOutputStream;
006 import java.io.DataInputStream;
007 import java.io.File;
008 import java.io.FileInputStream;
009 import java.io.FileNotFoundException;
010 import java.io.FileOutputStream;
011 import java.io.IOException;
012 import java.io.InputStream;
013 import java.io.InputStreamReader;
014 import java.util.ArrayList;
015 import java.util.Collections;
016 import java.util.Enumeration;
017 import java.util.HashSet;
018 import java.util.Hashtable;
019 import java.util.LinkedHashSet;
020 import java.util.List;
021 import java.util.Map.Entry;
022 import java.util.zip.ZipEntry;
023 import java.util.zip.ZipFile;
024 import java.util.zip.ZipOutputStream;
025
026 import org.objectweb.asm.ClassReader;
027 import org.objectweb.asm.ClassWriter;
028 import org.objectweb.asm.Type;
029 import org.objectweb.asm.tree.AnnotationNode;
030 import org.objectweb.asm.tree.ClassNode;
031 import org.objectweb.asm.tree.FieldNode;
032 import org.objectweb.asm.tree.MethodNode;
033
034 import com.google.common.base.Objects;
035 import com.google.common.collect.Lists;
036 import com.google.common.collect.Sets;
037
038 import cpw.mods.fml.relauncher.Side;
039 import cpw.mods.fml.relauncher.SideOnly;
040
041 public class MCPMerger
042 {
043 private static Hashtable<String, ClassInfo> clients = new Hashtable<String, ClassInfo>();
044 private static Hashtable<String, ClassInfo> shared = new Hashtable<String, ClassInfo>();
045 private static Hashtable<String, ClassInfo> servers = new Hashtable<String, ClassInfo>();
046 private static HashSet<String> copyToServer = new HashSet<String>();
047 private static HashSet<String> copyToClient = new HashSet<String>();
048 private static HashSet<String> dontAnnotate = new HashSet<String>();
049 private static final boolean DEBUG = false;
050
051 public static void main(String[] args)
052 {
053 if (args.length != 3)
054 {
055 System.out.println("Usage: MCPMerger <MapFile> <minecraft.jar> <minecraft_server.jar>");
056 System.exit(1);
057 }
058
059 File map_file = new File(args[0]);
060 File client_jar = new File(args[1]);
061 File server_jar = new File(args[2]);
062 File client_jar_tmp = new File(args[1] + ".MergeBack");
063 File server_jar_tmp = new File(args[2] + ".MergeBack");
064
065
066 if (client_jar_tmp.exists() && !client_jar_tmp.delete())
067 {
068 System.out.println("Could not delete temp file: " + client_jar_tmp);
069 }
070
071 if (server_jar_tmp.exists() && !server_jar_tmp.delete())
072 {
073 System.out.println("Could not delete temp file: " + server_jar_tmp);
074 }
075
076 if (!client_jar.exists())
077 {
078 System.out.println("Could not find minecraft.jar: " + client_jar);
079 System.exit(1);
080 }
081
082 if (!server_jar.exists())
083 {
084 System.out.println("Could not find minecraft_server.jar: " + server_jar);
085 System.exit(1);
086 }
087
088 if (!client_jar.renameTo(client_jar_tmp))
089 {
090 System.out.println("Could not rename file: " + client_jar + " -> " + client_jar_tmp);
091 System.exit(1);
092 }
093
094 if (!server_jar.renameTo(server_jar_tmp))
095 {
096 System.out.println("Could not rename file: " + server_jar + " -> " + server_jar_tmp);
097 System.exit(1);
098 }
099
100 if (!readMapFile(map_file))
101 {
102 System.out.println("Could not read map file: " + map_file);
103 System.exit(1);
104 }
105
106 try
107 {
108 processJar(client_jar_tmp, server_jar_tmp, client_jar, server_jar);
109 }
110 catch (IOException e)
111 {
112 e.printStackTrace();
113 System.exit(1);
114 }
115
116 if (!client_jar_tmp.delete())
117 {
118 System.out.println("Could not delete temp file: " + client_jar_tmp);
119 }
120
121 if (!server_jar_tmp.delete())
122 {
123 System.out.println("Could not delete temp file: " + server_jar_tmp);
124 }
125 }
126
127 private static boolean readMapFile(File mapFile)
128 {
129 try
130 {
131 FileInputStream fstream = new FileInputStream(mapFile);
132 DataInputStream in = new DataInputStream(fstream);
133 BufferedReader br = new BufferedReader(new InputStreamReader(in));
134
135 String line;
136 while ((line = br.readLine()) != null)
137 {
138 line = line.split("#")[0];
139 char cmd = line.charAt(0);
140 line = line.substring(1).trim();
141
142 switch (cmd)
143 {
144 case '!': dontAnnotate.add(line); break;
145 case '<': copyToClient.add(line); break;
146 case '>': copyToServer.add(line); break;
147 }
148 }
149
150 in.close();
151 return true;
152 }
153 catch (Exception e)
154 {
155 System.err.println("Error: " + e.getMessage());
156 return false;
157 }
158 }
159
160 public static void processJar(File clientInFile, File serverInFile, File clientOutFile, File serverOutFile) throws IOException
161 {
162 ZipFile cInJar = null;
163 ZipFile sInJar = null;
164 ZipOutputStream cOutJar = null;
165 ZipOutputStream sOutJar = null;
166
167 try
168 {
169 try
170 {
171 cInJar = new ZipFile(clientInFile);
172 sInJar = new ZipFile(serverInFile);
173 }
174 catch (FileNotFoundException e)
175 {
176 throw new FileNotFoundException("Could not open input file: " + e.getMessage());
177 }
178 try
179 {
180 cOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(clientOutFile)));
181 sOutJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(serverOutFile)));
182 }
183 catch (FileNotFoundException e)
184 {
185 throw new FileNotFoundException("Could not open output file: " + e.getMessage());
186 }
187 Hashtable<String, ZipEntry> cClasses = getClassEntries(cInJar, cOutJar);
188 Hashtable<String, ZipEntry> sClasses = getClassEntries(sInJar, sOutJar);
189 HashSet<String> cAdded = new HashSet<String>();
190 HashSet<String> sAdded = new HashSet<String>();
191
192 for (Entry<String, ZipEntry> entry : cClasses.entrySet())
193 {
194 String name = entry.getKey();
195 ZipEntry cEntry = entry.getValue();
196 ZipEntry sEntry = sClasses.get(name);
197
198 if (sEntry == null)
199 {
200 if (!copyToServer.contains(name))
201 {
202 copyClass(cInJar, cEntry, cOutJar, null, true);
203 cAdded.add(name);
204 }
205 else
206 {
207 if (DEBUG)
208 {
209 System.out.println("Copy class c->s : " + name);
210 }
211 copyClass(cInJar, cEntry, cOutJar, sOutJar, true);
212 cAdded.add(name);
213 sAdded.add(name);
214 }
215 continue;
216 }
217
218 sClasses.remove(name);
219 ClassInfo info = new ClassInfo(name);
220 shared.put(name, info);
221
222 byte[] cData = readEntry(cInJar, entry.getValue());
223 byte[] sData = readEntry(sInJar, sEntry);
224 byte[] data = processClass(cData, sData, info);
225
226 ZipEntry newEntry = new ZipEntry(cEntry.getName());
227 cOutJar.putNextEntry(newEntry);
228 cOutJar.write(data);
229 sOutJar.putNextEntry(newEntry);
230 sOutJar.write(data);
231 cAdded.add(name);
232 sAdded.add(name);
233 }
234
235 for (Entry<String, ZipEntry> entry : sClasses.entrySet())
236 {
237 if (DEBUG)
238 {
239 System.out.println("Copy class s->c : " + entry.getKey());
240 }
241 copyClass(sInJar, entry.getValue(), cOutJar, sOutJar, false);
242 }
243
244 for (String name : new String[]{SideOnly.class.getName(), Side.class.getName()})
245 {
246 String eName = name.replace(".", "/");
247 byte[] data = getClassBytes(name);
248 ZipEntry newEntry = new ZipEntry(name.replace(".", "/").concat(".class"));
249 if (!cAdded.contains(eName))
250 {
251 cOutJar.putNextEntry(newEntry);
252 cOutJar.write(data);
253 }
254 if (!sAdded.contains(eName))
255 {
256 sOutJar.putNextEntry(newEntry);
257 sOutJar.write(data);
258 }
259 }
260
261 }
262 finally
263 {
264 if (cInJar != null)
265 {
266 try { cInJar.close(); } catch (IOException e){}
267 }
268
269 if (sInJar != null)
270 {
271 try { sInJar.close(); } catch (IOException e) {}
272 }
273 if (cOutJar != null)
274 {
275 try { cOutJar.close(); } catch (IOException e){}
276 }
277
278 if (sOutJar != null)
279 {
280 try { sOutJar.close(); } catch (IOException e) {}
281 }
282 }
283 }
284
285 private static void copyClass(ZipFile inJar, ZipEntry entry, ZipOutputStream outJar, ZipOutputStream outJar2, boolean isClientOnly) throws IOException
286 {
287 ClassReader reader = new ClassReader(readEntry(inJar, entry));
288 ClassNode classNode = new ClassNode();
289
290 reader.accept(classNode, 0);
291
292 if (!dontAnnotate.contains(classNode.name))
293 {
294 if (classNode.visibleAnnotations == null) classNode.visibleAnnotations = new ArrayList<AnnotationNode>();
295 classNode.visibleAnnotations.add(getSideAnn(isClientOnly));
296 }
297
298 ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
299 classNode.accept(writer);
300 byte[] data = writer.toByteArray();
301
302 ZipEntry newEntry = new ZipEntry(entry.getName());
303 if (outJar != null)
304 {
305 outJar.putNextEntry(newEntry);
306 outJar.write(data);
307 }
308 if (outJar2 != null)
309 {
310 outJar2.putNextEntry(newEntry);
311 outJar2.write(data);
312 }
313 }
314
315 private static AnnotationNode getSideAnn(boolean isClientOnly)
316 {
317 AnnotationNode ann = new AnnotationNode(Type.getDescriptor(SideOnly.class));
318 ann.values = new ArrayList<Object>();
319 ann.values.add("value");
320 ann.values.add(new String[]{ Type.getDescriptor(Side.class), (isClientOnly ? "CLIENT" : "SERVER")});
321 return ann;
322 }
323
324 @SuppressWarnings("unchecked")
325 private static Hashtable<String, ZipEntry> getClassEntries(ZipFile inFile, ZipOutputStream outFile) throws IOException
326 {
327 Hashtable<String, ZipEntry> ret = new Hashtable<String, ZipEntry>();
328 for (ZipEntry entry : Collections.list((Enumeration<ZipEntry>)inFile.entries()))
329 {
330 if (entry.isDirectory())
331 {
332 outFile.putNextEntry(entry);
333 continue;
334 }
335 String entryName = entry.getName();
336 if (!entryName.endsWith(".class") || entryName.startsWith("."))
337 {
338 ZipEntry newEntry = new ZipEntry(entry.getName());
339 outFile.putNextEntry(newEntry);
340 outFile.write(readEntry(inFile, entry));
341 }
342 else
343 {
344 ret.put(entryName.replace(".class", ""), entry);
345 }
346 }
347 return ret;
348 }
349 private static byte[] readEntry(ZipFile inFile, ZipEntry entry) throws IOException
350 {
351 return readFully(inFile.getInputStream(entry));
352 }
353 private static byte[] readFully(InputStream stream) throws IOException
354 {
355 byte[] data = new byte[4096];
356 ByteArrayOutputStream entryBuffer = new ByteArrayOutputStream();
357 int len;
358 do
359 {
360 len = stream.read(data);
361 if (len > 0)
362 {
363 entryBuffer.write(data, 0, len);
364 }
365 } while (len != -1);
366
367 return entryBuffer.toByteArray();
368 }
369 private static class ClassInfo
370 {
371 public String name;
372 public ArrayList<FieldNode> cField = new ArrayList<FieldNode>();
373 public ArrayList<FieldNode> sField = new ArrayList<FieldNode>();
374 public ArrayList<MethodNode> cMethods = new ArrayList<MethodNode>();
375 public ArrayList<MethodNode> sMethods = new ArrayList<MethodNode>();
376 public ClassInfo(String name){ this.name = name; }
377 public boolean isSame() { return (cField.size() == 0 && sField.size() == 0 && cMethods.size() == 0 && sMethods.size() == 0); }
378 }
379
380 public static byte[] processClass(byte[] cIn, byte[] sIn, ClassInfo info)
381 {
382 ClassNode cClassNode = getClassNode(cIn);
383 ClassNode sClassNode = getClassNode(sIn);
384
385 processFields(cClassNode, sClassNode, info);
386 processMethods(cClassNode, sClassNode, info);
387
388 ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
389 cClassNode.accept(writer);
390 return writer.toByteArray();
391 }
392
393 private static ClassNode getClassNode(byte[] data)
394 {
395 ClassReader reader = new ClassReader(data);
396 ClassNode classNode = new ClassNode();
397 reader.accept(classNode, 0);
398 return classNode;
399 }
400
401 @SuppressWarnings("unchecked")
402 private static void processFields(ClassNode cClass, ClassNode sClass, ClassInfo info)
403 {
404 List<FieldNode> cFields = cClass.fields;
405 List<FieldNode> sFields = sClass.fields;
406
407 int sI = 0;
408 for (int x = 0; x < cFields.size(); x++)
409 {
410 FieldNode cF = cFields.get(x);
411 if (sI < sFields.size())
412 {
413 if (!cF.name.equals(sFields.get(sI).name))
414 {
415 boolean serverHas = false;
416 for (int y = sI + 1; y < sFields.size(); y++)
417 {
418 if (cF.name.equals(sFields.get(y).name))
419 {
420 serverHas = true;
421 break;
422 }
423 }
424 if (serverHas)
425 {
426 boolean clientHas = false;
427 FieldNode sF = sFields.get(sI);
428 for (int y = x + 1; y < cFields.size(); y++)
429 {
430 if (sF.name.equals(cFields.get(y).name))
431 {
432 clientHas = true;
433 break;
434 }
435 }
436 if (!clientHas)
437 {
438 if (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
439 sF.visibleAnnotations.add(getSideAnn(false));
440 cFields.add(x++, sF);
441 info.sField.add(sF);
442 }
443 }
444 else
445 {
446 if (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
447 cF.visibleAnnotations.add(getSideAnn(true));
448 sFields.add(sI, cF);
449 info.cField.add(cF);
450 }
451 }
452 }
453 else
454 {
455 if (cF.visibleAnnotations == null) cF.visibleAnnotations = new ArrayList<AnnotationNode>();
456 cF.visibleAnnotations.add(getSideAnn(true));
457 sFields.add(sI, cF);
458 info.cField.add(cF);
459 }
460 sI++;
461 }
462 if (sFields.size() != cFields.size())
463 {
464 for (int x = cFields.size(); x < sFields.size(); x++)
465 {
466 FieldNode sF = sFields.get(x);
467 if (sF.visibleAnnotations == null) sF.visibleAnnotations = new ArrayList<AnnotationNode>();
468 sF.visibleAnnotations.add(getSideAnn(true));
469 cFields.add(x++, sF);
470 info.sField.add(sF);
471 }
472 }
473 }
474
475 private static class MethodWrapper
476 {
477 private MethodNode node;
478 public boolean client;
479 public boolean server;
480 public MethodWrapper(MethodNode node)
481 {
482 this.node = node;
483 }
484 @Override
485 public boolean equals(Object obj)
486 {
487 if (obj == null || !(obj instanceof MethodWrapper)) return false;
488 MethodWrapper mw = (MethodWrapper) obj;
489 boolean eq = Objects.equal(node.name, mw.node.name) && Objects.equal(node.desc, mw.node.desc);
490 if (eq)
491 {
492 mw.client = this.client | mw.client;
493 mw.server = this.server | mw.server;
494 this.client = this.client | mw.client;
495 this.server = this.server | mw.server;
496 if (DEBUG)
497 {
498 System.out.printf(" eq: %s %s\n", this, mw);
499 }
500 }
501 return eq;
502 }
503
504 @Override
505 public int hashCode()
506 {
507 return Objects.hashCode(node.name, node.desc);
508 }
509 @Override
510 public String toString()
511 {
512 return Objects.toStringHelper(this).add("name", node.name).add("desc",node.desc).add("server",server).add("client",client).toString();
513 }
514 }
515 @SuppressWarnings("unchecked")
516 private static void processMethods(ClassNode cClass, ClassNode sClass, ClassInfo info)
517 {
518 List<MethodNode> cMethods = (List<MethodNode>)cClass.methods;
519 List<MethodNode> sMethods = (List<MethodNode>)sClass.methods;
520 LinkedHashSet<MethodWrapper> allMethods = Sets.newLinkedHashSet();
521
522 int cPos = 0;
523 int sPos = 0;
524 int cLen = cMethods.size();
525 int sLen = sMethods.size();
526 String clientName = "";
527 String lastName = clientName;
528 String serverName = "";
529 while (cPos < cLen || sPos < sLen)
530 {
531 do
532 {
533 if (sPos>=sLen)
534 {
535 break;
536 }
537 MethodNode sM = sMethods.get(sPos);
538 serverName = sM.name;
539 if (!serverName.equals(lastName) && cPos != cLen)
540 {
541 if (DEBUG)
542 {
543 System.out.printf("Server -skip : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
544 }
545 break;
546 }
547 MethodWrapper mw = new MethodWrapper(sM);
548 mw.server = true;
549 allMethods.add(mw);
550 if (DEBUG)
551 {
552 System.out.printf("Server *add* : %s %s %d (%s %d) %d [%s]\n", sClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
553 }
554 sPos++;
555 }
556 while (sPos < sLen);
557 do
558 {
559 if (cPos>=cLen)
560 {
561 break;
562 }
563 MethodNode cM = cMethods.get(cPos);
564 lastName = clientName;
565 clientName = cM.name;
566 if (!clientName.equals(lastName) && sPos != sLen)
567 {
568 if (DEBUG)
569 {
570 System.out.printf("Client -skip : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
571 }
572 break;
573 }
574 MethodWrapper mw = new MethodWrapper(cM);
575 mw.client = true;
576 allMethods.add(mw);
577 if (DEBUG)
578 {
579 System.out.printf("Client *add* : %s %s %d (%s %d) %d [%s]\n", cClass.name, clientName, cLen - cPos, serverName, sLen - sPos, allMethods.size(), lastName);
580 }
581 cPos++;
582 }
583 while (cPos < cLen);
584 }
585
586 cMethods.clear();
587 sMethods.clear();
588
589 for (MethodWrapper mw : allMethods)
590 {
591 if (DEBUG)
592 {
593 System.out.println(mw);
594 }
595 cMethods.add(mw.node);
596 sMethods.add(mw.node);
597 if (mw.server && mw.client)
598 {
599 // no op
600 }
601 else
602 {
603 if (mw.node.visibleAnnotations == null) mw.node.visibleAnnotations = Lists.newArrayListWithExpectedSize(1);
604 mw.node.visibleAnnotations.add(getSideAnn(mw.client));
605 if (mw.client)
606 {
607 info.sMethods.add(mw.node);
608 }
609 else
610 {
611 info.cMethods.add(mw.node);
612 }
613 }
614 }
615 }
616
617 public static byte[] getClassBytes(String name) throws IOException
618 {
619 InputStream classStream = null;
620 try
621 {
622 classStream = MCPMerger.class.getResourceAsStream("/" + name.replace('.', '/').concat(".class"));
623 return readFully(classStream);
624 }
625 finally
626 {
627 if (classStream != null)
628 {
629 try
630 {
631 classStream.close();
632 }
633 catch (IOException e){}
634 }
635 }
636 }
637 }