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