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: AccessTransformer <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("ayn")) //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    }