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