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 static org.objectweb.asm.Opcodes.ACC_FINAL;
016import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
017import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
018import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
019
020import java.io.BufferedInputStream;
021import java.io.BufferedOutputStream;
022import java.io.ByteArrayOutputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileNotFoundException;
026import java.io.FileOutputStream;
027import java.io.IOException;
028import java.net.URL;
029import java.util.Collection;
030import java.util.List;
031import java.util.zip.ZipEntry;
032import java.util.zip.ZipInputStream;
033import java.util.zip.ZipOutputStream;
034
035import org.objectweb.asm.ClassReader;
036import org.objectweb.asm.ClassWriter;
037import org.objectweb.asm.tree.ClassNode;
038import org.objectweb.asm.tree.FieldNode;
039import org.objectweb.asm.tree.MethodNode;
040
041import com.google.common.base.Charsets;
042import com.google.common.base.Splitter;
043import com.google.common.collect.ArrayListMultimap;
044import com.google.common.collect.Iterables;
045import com.google.common.collect.Lists;
046import com.google.common.collect.Multimap;
047import com.google.common.io.LineProcessor;
048import com.google.common.io.Resources;
049
050import cpw.mods.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper;
051import cpw.mods.fml.relauncher.IClassTransformer;
052
053public class AccessTransformer implements IClassTransformer
054{
055    private static final boolean DEBUG = false;
056    private class Modifier
057    {
058        public String name = "";
059        public String desc = "";
060        public int oldAccess = 0;
061        public int newAccess = 0;
062        public int targetAccess = 0;
063        public boolean changeFinal = false;
064        public boolean markFinal = false;
065        protected boolean modifyClassVisibility;
066
067        private void setTargetAccess(String name)
068        {
069            if (name.startsWith("public")) targetAccess = ACC_PUBLIC;
070            else if (name.startsWith("private")) targetAccess = ACC_PRIVATE;
071            else if (name.startsWith("protected")) targetAccess = ACC_PROTECTED;
072
073            if (name.endsWith("-f"))
074            {
075                changeFinal = true;
076                markFinal = false;
077            }
078            else if (name.endsWith("+f"))
079            {
080                changeFinal = true;
081                markFinal = true;
082            }
083        }
084    }
085
086    private Multimap<String, Modifier> modifiers = ArrayListMultimap.create();
087
088    public AccessTransformer() throws IOException
089    {
090        this("fml_at.cfg");
091    }
092    protected AccessTransformer(String rulesFile) throws IOException
093    {
094        readMapFile(rulesFile);
095    }
096
097    private void readMapFile(String rulesFile) throws IOException
098    {
099        File file = new File(rulesFile);
100        URL rulesResource;
101        if (file.exists())
102        {
103            rulesResource = file.toURI().toURL();
104        }
105        else
106        {
107            rulesResource = Resources.getResource(rulesFile);
108        }
109        Resources.readLines(rulesResource, Charsets.UTF_8, new LineProcessor<Void>()
110        {
111            @Override
112            public Void getResult()
113            {
114                return null;
115            }
116
117            @Override
118            public boolean processLine(String input) throws IOException
119            {
120                String line = Iterables.getFirst(Splitter.on('#').limit(2).split(input), "").trim();
121                if (line.length()==0)
122                {
123                    return true;
124                }
125                List<String> parts = Lists.newArrayList(Splitter.on(" ").trimResults().split(line));
126                if (parts.size()>2)
127                {
128                    throw new RuntimeException("Invalid config file line "+ input);
129                }
130                Modifier m = new Modifier();
131                m.setTargetAccess(parts.get(0));
132                List<String> descriptor = Lists.newArrayList(Splitter.on(".").trimResults().split(parts.get(1)));
133                if (descriptor.size() == 1)
134                {
135                    m.modifyClassVisibility = true;
136                }
137                else
138                {
139                    String nameReference = descriptor.get(1);
140                    int parenIdx = nameReference.indexOf('(');
141                    if (parenIdx>0)
142                    {
143                        m.desc = nameReference.substring(parenIdx);
144                        m.name = nameReference.substring(0,parenIdx);
145                    }
146                    else
147                    {
148                        m.name = nameReference;
149                    }
150                }
151                modifiers.put(descriptor.get(0).replace('/', '.'), m);
152                return true;
153            }
154        });
155    }
156
157    @SuppressWarnings("unchecked")
158    @Override
159    public byte[] transform(String name, String transformedName, byte[] bytes)
160    {
161        if (bytes == null) { return null; }
162        boolean makeAllPublic = FMLDeobfuscatingRemapper.INSTANCE.isRemappedClass(name);
163
164        if (DEBUG)
165        {
166            System.out.printf("Considering all methods and fields on %s (%s): %b\n", name, transformedName, makeAllPublic);
167        }
168        if (!makeAllPublic && !modifiers.containsKey(name)) { return bytes; }
169
170        ClassNode classNode = new ClassNode();
171        ClassReader classReader = new ClassReader(bytes);
172        classReader.accept(classNode, 0);
173
174        if (makeAllPublic)
175        {
176            // class
177            Modifier m = new Modifier();
178            m.targetAccess = ACC_PUBLIC;
179            m.modifyClassVisibility = true;
180            modifiers.put(name,m);
181            // fields
182            m = new Modifier();
183            m.targetAccess = ACC_PUBLIC;
184            m.name = "*";
185            modifiers.put(name,m);
186            // methods
187            m = new Modifier();
188            m.targetAccess = ACC_PUBLIC;
189            m.name = "*";
190            m.desc = "<dummy>";
191            modifiers.put(name,m);
192            if (DEBUG)
193            {
194                System.out.printf("Injected all public modifiers for %s (%s)\n", name, transformedName);
195            }
196        }
197
198        Collection<Modifier> mods = modifiers.get(name);
199        for (Modifier m : mods)
200        {
201            if (m.modifyClassVisibility)
202            {
203                classNode.access = getFixedAccess(classNode.access, m);
204                if (DEBUG)
205                {
206                    System.out.println(String.format("Class: %s %s -> %s", name, toBinary(m.oldAccess), toBinary(m.newAccess)));
207                }
208                continue;
209            }
210            if (m.desc.isEmpty())
211            {
212                for (FieldNode n : (List<FieldNode>) classNode.fields)
213                {
214                    if (n.name.equals(m.name) || m.name.equals("*"))
215                    {
216                        n.access = getFixedAccess(n.access, m);
217                        if (DEBUG)
218                        {
219                            System.out.println(String.format("Field: %s.%s %s -> %s", name, n.name, toBinary(m.oldAccess), toBinary(m.newAccess)));
220                        }
221
222                        if (!m.name.equals("*"))
223                        {
224                            break;
225                        }
226                    }
227                }
228            }
229            else
230            {
231                for (MethodNode n : (List<MethodNode>) classNode.methods)
232                {
233                    if ((n.name.equals(m.name) && n.desc.equals(m.desc)) || m.name.equals("*"))
234                    {
235                        n.access = getFixedAccess(n.access, m);
236                        if (DEBUG)
237                        {
238                            System.out.println(String.format("Method: %s.%s%s %s -> %s", name, n.name, n.desc, toBinary(m.oldAccess), toBinary(m.newAccess)));
239                        }
240
241                        if (!m.name.equals("*"))
242                        {
243                            break;
244                        }
245                    }
246                }
247            }
248        }
249
250        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
251        classNode.accept(writer);
252        return writer.toByteArray();
253    }
254
255    private String toBinary(int num)
256    {
257        return String.format("%16s", Integer.toBinaryString(num)).replace(' ', '0');
258    }
259
260    private int getFixedAccess(int access, Modifier target)
261    {
262        target.oldAccess = access;
263        int t = target.targetAccess;
264        int ret = (access & ~7);
265
266        switch (access & 7)
267        {
268        case ACC_PRIVATE:
269            ret |= t;
270            break;
271        case 0: // default
272            ret |= (t != ACC_PRIVATE ? t : 0 /* default */);
273            break;
274        case ACC_PROTECTED:
275            ret |= (t != ACC_PRIVATE && t != 0 /* default */? t : ACC_PROTECTED);
276            break;
277        case ACC_PUBLIC:
278            ret |= (t != ACC_PRIVATE && t != 0 /* default */&& t != ACC_PROTECTED ? t : ACC_PUBLIC);
279            break;
280        default:
281            throw new RuntimeException("The fuck?");
282        }
283
284        // Clear the "final" marker on fields only if specified in control field
285        if (target.changeFinal && target.desc == "")
286        {
287            if (target.markFinal)
288            {
289                ret |= ACC_FINAL;
290            }
291            else
292            {
293                ret &= ~ACC_FINAL;
294            }
295        }
296        target.newAccess = ret;
297        return ret;
298    }
299
300    public static void main(String[] args)
301    {
302        if (args.length < 2)
303        {
304            System.out.println("Usage: AccessTransformer <JarPath> <MapFile> [MapFile2]... ");
305            System.exit(1);
306        }
307
308        boolean hasTransformer = false;
309        AccessTransformer[] trans = new AccessTransformer[args.length - 1];
310        for (int x = 1; x < args.length; x++)
311        {
312            try
313            {
314                trans[x - 1] = new AccessTransformer(args[x]);
315                hasTransformer = true;
316            }
317            catch (IOException e)
318            {
319                System.out.println("Could not read Transformer Map: " + args[x]);
320                e.printStackTrace();
321            }
322        }
323
324        if (!hasTransformer)
325        {
326            System.out.println("Culd not find a valid transformer to perform");
327            System.exit(1);
328        }
329
330        File orig = new File(args[0]);
331        File temp = new File(args[0] + ".ATBack");
332        if (!orig.exists() && !temp.exists())
333        {
334            System.out.println("Could not find target jar: " + orig);
335            System.exit(1);
336        }
337
338        if (!orig.renameTo(temp))
339        {
340            System.out.println("Could not rename file: " + orig + " -> " + temp);
341            System.exit(1);
342        }
343
344        try
345        {
346            processJar(temp, orig, trans);
347        }
348        catch (IOException e)
349        {
350            e.printStackTrace();
351            System.exit(1);
352        }
353
354        if (!temp.delete())
355        {
356            System.out.println("Could not delete temp file: " + temp);
357        }
358    }
359
360    private static void processJar(File inFile, File outFile, AccessTransformer[] transformers) throws IOException
361    {
362        ZipInputStream inJar = null;
363        ZipOutputStream outJar = null;
364
365        try
366        {
367            try
368            {
369                inJar = new ZipInputStream(new BufferedInputStream(new FileInputStream(inFile)));
370            }
371            catch (FileNotFoundException e)
372            {
373                throw new FileNotFoundException("Could not open input file: " + e.getMessage());
374            }
375
376            try
377            {
378                outJar = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(outFile)));
379            }
380            catch (FileNotFoundException e)
381            {
382                throw new FileNotFoundException("Could not open output file: " + e.getMessage());
383            }
384
385            ZipEntry entry;
386            while ((entry = inJar.getNextEntry()) != null)
387            {
388                if (entry.isDirectory())
389                {
390                    outJar.putNextEntry(entry);
391                    continue;
392                }
393
394                byte[] data = new byte[4096];
395                ByteArrayOutputStream entryBuffer = new ByteArrayOutputStream();
396
397                int len;
398                do
399                {
400                    len = inJar.read(data);
401                    if (len > 0)
402                    {
403                        entryBuffer.write(data, 0, len);
404                    }
405                }
406                while (len != -1);
407
408                byte[] entryData = entryBuffer.toByteArray();
409
410                String entryName = entry.getName();
411
412                if (entryName.endsWith(".class") && !entryName.startsWith("."))
413                {
414                    ClassNode cls = new ClassNode();
415                    ClassReader rdr = new ClassReader(entryData);
416                    rdr.accept(cls, 0);
417                    String name = cls.name.replace('/', '.').replace('\\', '.');
418
419                    for (AccessTransformer trans : transformers)
420                    {
421                        entryData = trans.transform(name, name, entryData);
422                    }
423                }
424
425                ZipEntry newEntry = new ZipEntry(entryName);
426                outJar.putNextEntry(newEntry);
427                outJar.write(entryData);
428            }
429        }
430        finally
431        {
432            if (outJar != null)
433            {
434                try
435                {
436                    outJar.close();
437                }
438                catch (IOException e)
439                {
440                }
441            }
442
443            if (inJar != null)
444            {
445                try
446                {
447                    inJar.close();
448                }
449                catch (IOException e)
450                {
451                }
452            }
453        }
454    }
455    public void ensurePublicAccessFor(String modClazzName)
456    {
457        Modifier m = new Modifier();
458        m.setTargetAccess("public");
459        m.modifyClassVisibility = true;
460        modifiers.put(modClazzName, m);
461    }
462}