    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}