    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.deobf;
    014
    015import java.io.File;
    016import java.io.IOException;
    017import java.io.InputStream;
    018import java.io.InputStreamReader;
    019import java.nio.charset.Charset;
    020import java.util.Arrays;
    021import java.util.HashMap;
    022import java.util.List;
    023import java.util.Map;
    024import java.util.Set;
    025import java.util.logging.Level;
    026import java.util.zip.ZipEntry;
    027import java.util.zip.ZipFile;
    028
    029import org.objectweb.asm.ClassReader;
    030import org.objectweb.asm.commons.Remapper;
    031
    032import com.google.common.base.CharMatcher;
    033import com.google.common.base.Charsets;
    034import com.google.common.base.Splitter;
    035import com.google.common.base.Strings;
    036import com.google.common.collect.BiMap;
    037import com.google.common.collect.HashBiMap;
    038import com.google.common.collect.ImmutableBiMap;
    039import com.google.common.collect.ImmutableList;
    040import com.google.common.collect.ImmutableMap;
    041import com.google.common.collect.Iterables;
    042import com.google.common.collect.ListMultimap;
    043import com.google.common.collect.Lists;
    044import com.google.common.collect.Maps;
    045import com.google.common.collect.Sets;
    046import com.google.common.collect.ImmutableBiMap.Builder;
    047import com.google.common.io.CharStreams;
    048import com.google.common.io.InputSupplier;
    049
    050import cpw.mods.fml.common.FMLLog;
    051import cpw.mods.fml.relauncher.FMLRelaunchLog;
    052import cpw.mods.fml.relauncher.RelaunchClassLoader;
    053import org.objectweb.asm.tree.ClassNode;
    054import org.objectweb.asm.tree.FieldNode;
    055
    056public class FMLDeobfuscatingRemapper extends Remapper {
    057    public static final FMLDeobfuscatingRemapper INSTANCE = new FMLDeobfuscatingRemapper();
    058
    059    private BiMap<String, String> classNameBiMap;
    060    private BiMap<String, String> mcpNameBiMap;
    061
    062    private Map<String,Map<String,String>> rawFieldMaps;
    063    private Map<String,Map<String,String>> rawMethodMaps;
    064
    065    private Map<String,Map<String,String>> fieldNameMaps;
    066    private Map<String,Map<String,String>> methodNameMaps;
    067
    068    private RelaunchClassLoader classLoader;
    069
    070    private FMLDeobfuscatingRemapper()
    071    {
    072        classNameBiMap=ImmutableBiMap.of();
    073        mcpNameBiMap=ImmutableBiMap.of();
    074    }
    075
    076    public void setup(File mcDir, RelaunchClassLoader classLoader, String deobfFileName)
    077    {
    078        this.classLoader = classLoader;
    079        try
    080        {
    081            File libDir = new File(mcDir, "lib");
    082            File mapData = new File(libDir, deobfFileName);
    083            mapData = mapData.getCanonicalFile();
    084            ZipFile mapZip = new ZipFile(mapData);
    085            ZipEntry classData = mapZip.getEntry("joined.srg");
    086            ZipInputSupplier zis = new ZipInputSupplier(mapZip, classData);
    087            InputSupplier<InputStreamReader> srgSupplier = CharStreams.newReaderSupplier(zis,Charsets.UTF_8);
    088            List<String> srgList = CharStreams.readLines(srgSupplier);
    089            rawMethodMaps = Maps.newHashMap();
    090            rawFieldMaps = Maps.newHashMap();
    091            Builder<String, String> builder = ImmutableBiMap.<String,String>builder();
    092            Builder<String, String> mcpBuilder = ImmutableBiMap.<String,String>builder();
    093            Splitter splitter = Splitter.on(CharMatcher.anyOf(": ")).omitEmptyStrings().trimResults();
    094            for (String line : srgList)
    095            {
    096                String[] parts = Iterables.toArray(splitter.split(line),String.class);
    097                String typ = parts[0];
    098                if ("CL".equals(typ))
    099                {
    100                    parseClass(builder, parts);
    101                    parseMCPClass(mcpBuilder,parts);
    102                }
    103                else if ("MD".equals(typ))
    104                {
    105                    parseMethod(parts);
    106                }
    107                else if ("FD".equals(typ))
    108                {
    109                    parseField(parts);
    110                }
    111            }
    112            classNameBiMap = builder.build();
    113            // Special case some mappings for modloader mods
    114            mcpBuilder.put("BaseMod","net/minecraft/src/BaseMod");
    115            mcpBuilder.put("ModLoader","net/minecraft/src/ModLoader");
    116            mcpBuilder.put("EntityRendererProxy","net/minecraft/src/EntityRendererProxy");
    117            mcpBuilder.put("MLProp","net/minecraft/src/MLProp");
    118            mcpBuilder.put("TradeEntry","net/minecraft/src/TradeEntry");
    119            mcpNameBiMap = mcpBuilder.build();
    120        }
    121        catch (IOException ioe)
    122        {
    123            FMLRelaunchLog.log(Level.SEVERE, ioe, "An error occurred loading the deobfuscation map data");
    124        }
    125        methodNameMaps = Maps.newHashMapWithExpectedSize(rawMethodMaps.size());
    126        fieldNameMaps = Maps.newHashMapWithExpectedSize(rawFieldMaps.size());
    127    }
    128
    129    public boolean isRemappedClass(String className)
    130    {
    131        className = className.replace('.', '/');
    132        return classNameBiMap.containsKey(className) || mcpNameBiMap.containsKey(className) || (!classNameBiMap.isEmpty() && className.indexOf('/') == -1);
    133    }
    134
    135    private void parseField(String[] parts)
    136    {
    137        String oldSrg = parts[1];
    138        int lastOld = oldSrg.lastIndexOf('/');
    139        String cl = oldSrg.substring(0,lastOld);
    140        String oldName = oldSrg.substring(lastOld+1);
    141        String newSrg = parts[2];
    142        int lastNew = newSrg.lastIndexOf('/');
    143        String newName = newSrg.substring(lastNew+1);
    144        if (!rawFieldMaps.containsKey(cl))
    145        {
    146            rawFieldMaps.put(cl, Maps.<String,String>newHashMap());
    147        }
    148        rawFieldMaps.get(cl).put(oldName + ":" + getFieldType(cl, oldName), newName);
    149        rawFieldMaps.get(cl).put(oldName + ":null", newName);
    150    }
    151
    152    /*
    153     * Cache the field descriptions for classes so we don't repeatedly reload the same data again and again
    154     */
    155    private Map<String,Map<String,String>> fieldDescriptions = Maps.newHashMap();
    156
    157    private String getFieldType(String owner, String name)
    158    {
    159        if (fieldDescriptions.containsKey(owner))
    160        {
    161            return fieldDescriptions.get(owner).get(name);
    162        }
    163        synchronized (fieldDescriptions)
    164        {
    165            try
    166            {
    167                byte[] classBytes = classLoader.getClassBytes(owner);
    168                if (classBytes == null)
    169                {
    170                    return null;
    171                }
    172                ClassReader cr = new ClassReader(classBytes);
    173                ClassNode classNode = new ClassNode();
    174                cr.accept(classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
    175                Map<String,String> resMap = Maps.newHashMap();
    176                for (FieldNode fieldNode : (List<FieldNode>) classNode.fields) {
    177                    resMap.put(fieldNode.name, fieldNode.desc);
    178                }
    179                fieldDescriptions.put(owner, resMap);
    180                return resMap.get(name);
    181            }
    182            catch (IOException e)
    183            {
    184                FMLLog.log(Level.SEVERE,e, "A critical exception occured reading a class file %s", owner);
    185            }
    186            return null;
    187        }
    188    }
    189
    190    private void parseClass(Builder<String, String> builder, String[] parts)
    191    {
    192        builder.put(parts[1],parts[2]);
    193    }
    194
    195    private void parseMCPClass(Builder<String, String> builder, String[] parts)
    196    {
    197        int clIdx = parts[2].lastIndexOf('/');
    198        builder.put("net/minecraft/src/"+parts[2].substring(clIdx+1),parts[2]);
    199    }
    200
    201    private void parseMethod(String[] parts)
    202    {
    203        String oldSrg = parts[1];
    204        int lastOld = oldSrg.lastIndexOf('/');
    205        String cl = oldSrg.substring(0,lastOld);
    206        String oldName = oldSrg.substring(lastOld+1);
    207        String sig = parts[2];
    208        String newSrg = parts[3];
    209        int lastNew = newSrg.lastIndexOf('/');
    210        String newName = newSrg.substring(lastNew+1);
    211        if (!rawMethodMaps.containsKey(cl))
    212        {
    213            rawMethodMaps.put(cl, Maps.<String,String>newHashMap());
    214        }
    215        rawMethodMaps.get(cl).put(oldName+sig, newName);
    216    }
    217
    218    @Override
    219    public String mapFieldName(String owner, String name, String desc)
    220    {
    221        if (classNameBiMap == null || classNameBiMap.isEmpty())
    222        {
    223            return name;
    224        }
    225        Map<String, String> fieldMap = getFieldMap(owner);
    226        return fieldMap!=null && fieldMap.containsKey(name+":"+desc) ? fieldMap.get(name+":"+desc) : name;
    227    }
    228
    229    @Override
    230    public String map(String typeName)
    231    {
    232        if (classNameBiMap == null || classNameBiMap.isEmpty())
    233        {
    234            return typeName;
    235        }
    236
    237        int dollarIdx = typeName.indexOf('$');
    238        String realType = dollarIdx > -1 ? typeName.substring(0, dollarIdx) : typeName;
    239        String subType = dollarIdx > -1 ? typeName.substring(dollarIdx+1) : "";
    240
    241        String result = classNameBiMap.containsKey(realType) ? classNameBiMap.get(realType) : mcpNameBiMap.containsKey(realType) ? mcpNameBiMap.get(realType) : realType;
    242        result = dollarIdx > -1 ? result+"$"+subType : result;
    243//        System.out.printf("Mapping %s=>%s\n",typeName,result);
    244        return result;
    245    }
    246
    247    public String unmap(String typeName)
    248    {
    249        if (classNameBiMap == null || classNameBiMap.isEmpty())
    250        {
    251            return typeName;
    252        }
    253        int dollarIdx = typeName.indexOf('$');
    254        String realType = dollarIdx > -1 ? typeName.substring(0, dollarIdx) : typeName;
    255        String subType = dollarIdx > -1 ? typeName.substring(dollarIdx+1) : "";
    256
    257
    258        String result = classNameBiMap.containsValue(realType) ? classNameBiMap.inverse().get(realType) : mcpNameBiMap.containsValue(realType) ? mcpNameBiMap.inverse().get(realType) : realType;
    259        result = dollarIdx > -1 ? result+"$"+subType : result;
    260//        System.out.printf("Unmapping %s=>%s\n",typeName,result);
    261        return result;
    262    }
    263
    264
    265    @Override
    266    public String mapMethodName(String owner, String name, String desc)
    267    {
    268        if (classNameBiMap==null || classNameBiMap.isEmpty())
    269        {
    270            return name;
    271        }
    272        Map<String, String> methodMap = getMethodMap(owner);
    273        String methodDescriptor = name+desc;
    274        return methodMap!=null && methodMap.containsKey(methodDescriptor) ? methodMap.get(methodDescriptor) : name;
    275    }
    276
    277    private Map<String,String> getFieldMap(String className)
    278    {
    279        if (!fieldNameMaps.containsKey(className))
    280        {
    281            findAndMergeSuperMaps(className);
    282        }
    283        return fieldNameMaps.get(className);
    284    }
    285
    286    private Map<String,String> getMethodMap(String className)
    287    {
    288        if (!methodNameMaps.containsKey(className))
    289        {
    290            findAndMergeSuperMaps(className);
    291        }
    292        return methodNameMaps.get(className);
    293    }
    294
    295    private void findAndMergeSuperMaps(String name)
    296    {
    297        try
    298        {
    299            byte[] classBytes = classLoader.getClassBytes(name);
    300            if (classBytes == null)
    301            {
    302                return;
    303            }
    304            ClassReader cr = new ClassReader(classBytes);
    305            String superName = cr.getSuperName();
    306            String[] interfaces = cr.getInterfaces();
    307            if (interfaces == null)
    308            {
    309                interfaces = new String[0];
    310            }
    311            mergeSuperMaps(name, superName, interfaces);
    312        }
    313        catch (IOException e)
    314        {
    315            e.printStackTrace();
    316        }
    317    }
    318    public void mergeSuperMaps(String name, String superName, String[] interfaces)
    319    {
    320//        System.out.printf("Computing super maps for %s: %s %s\n", name, superName, Arrays.asList(interfaces));
    321        if (classNameBiMap == null || classNameBiMap.isEmpty())
    322        {
    323            return;
    324        }
    325        // Skip Object
    326        if (Strings.isNullOrEmpty(superName))
    327        {
    328            return;
    329        }
    330
    331        List<String> allParents = ImmutableList.<String>builder().add(superName).addAll(Arrays.asList(interfaces)).build();
    332        // generate maps for all parent objects
    333        for (String parentThing : allParents)
    334        {
    335            if (!methodNameMaps.containsKey(parentThing))
    336            {
    337                findAndMergeSuperMaps(parentThing);
    338            }
    339        }
    340        Map<String, String> methodMap = Maps.<String,String>newHashMap();
    341        Map<String, String> fieldMap = Maps.<String,String>newHashMap();
    342        for (String parentThing : allParents)
    343        {
    344            if (methodNameMaps.containsKey(parentThing))
    345            {
    346                methodMap.putAll(methodNameMaps.get(parentThing));
    347            }
    348            if (fieldNameMaps.containsKey(parentThing))
    349            {
    350                fieldMap.putAll(fieldNameMaps.get(parentThing));
    351            }
    352        }
    353        if (rawMethodMaps.containsKey(name))
    354        {
    355            methodMap.putAll(rawMethodMaps.get(name));
    356        }
    357        if (rawFieldMaps.containsKey(name))
    358        {
    359            fieldMap.putAll(rawFieldMaps.get(name));
    360        }
    361        methodNameMaps.put(name, ImmutableMap.copyOf(methodMap));
    362        fieldNameMaps.put(name, ImmutableMap.copyOf(fieldMap));
    363//        System.out.printf("Maps: %s %s\n", name, methodMap);
    364    }
    365}