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 : 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}