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.relauncher;
014
015import java.io.ByteArrayOutputStream;
016import java.io.IOException;
017import java.io.InputStream;
018import java.net.JarURLConnection;
019import java.net.URL;
020import java.net.URLClassLoader;
021import java.net.URLConnection;
022import java.security.CodeSigner;
023import java.security.CodeSource;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.Set;
033import java.util.jar.Attributes.Name;
034import java.util.jar.Attributes;
035import java.util.jar.JarEntry;
036import java.util.jar.JarFile;
037import java.util.jar.Manifest;
038import java.util.logging.Level;
039
040import cpw.mods.fml.common.FMLLog;
041
042public class RelaunchClassLoader extends URLClassLoader
043{
044    private List<URL> sources;
045    private ClassLoader parent;
046
047    private List<IClassTransformer> transformers;
048    private Map<String, Class> cachedClasses;
049    private Set<String> invalidClasses;
050
051    private Set<String> classLoaderExceptions = new HashSet<String>();
052    private Set<String> transformerExceptions = new HashSet<String>();
053    private Map<Package,Manifest> packageManifests = new HashMap<Package,Manifest>();
054    private IClassNameTransformer renameTransformer;
055
056    private static Manifest EMPTY = new Manifest();
057
058    private ThreadLocal<byte[]> loadBuffer = new ThreadLocal<byte[]>();
059
060    private static final String[] RESERVED = {"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"};
061
062    private static final boolean DEBUG_CLASSLOADING = Boolean.parseBoolean(System.getProperty("fml.debugClassLoading", "false"));
063    private static final boolean DEBUG_CLASSLOADING_FINER = DEBUG_CLASSLOADING && Boolean.parseBoolean(System.getProperty("fml.debugClassLoadingFiner", "false"));
064
065    public RelaunchClassLoader(URL[] sources)
066    {
067        super(sources, null);
068        this.sources = new ArrayList<URL>(Arrays.asList(sources));
069        this.parent = getClass().getClassLoader();
070        this.cachedClasses = new HashMap<String,Class>(1000);
071        this.invalidClasses = new HashSet<String>(1000);
072        this.transformers = new ArrayList<IClassTransformer>(2);
073//        ReflectionHelper.setPrivateValue(ClassLoader.class, null, this, "scl");
074        Thread.currentThread().setContextClassLoader(this);
075
076        // standard classloader exclusions
077        addClassLoaderExclusion("java.");
078        addClassLoaderExclusion("sun.");
079        addClassLoaderExclusion("org.lwjgl.");
080        addClassLoaderExclusion("cpw.mods.fml.relauncher.");
081        addClassLoaderExclusion("net.minecraftforge.classloading.");
082
083        // standard transformer exclusions
084        addTransformerExclusion("javax.");
085        addTransformerExclusion("argo.");
086        addTransformerExclusion("org.objectweb.asm.");
087        addTransformerExclusion("com.google.common.");
088        addTransformerExclusion("org.bouncycastle.");
089        addTransformerExclusion("cpw.mods.fml.common.asm.transformers.deobf.");
090    }
091
092    public void registerTransformer(String transformerClassName)
093    {
094        try
095        {
096            IClassTransformer transformer = (IClassTransformer) loadClass(transformerClassName).newInstance();
097            transformers.add(transformer);
098            if (transformer instanceof IClassNameTransformer && renameTransformer == null)
099            {
100                renameTransformer = (IClassNameTransformer) transformer;
101            }
102        }
103        catch (Exception e)
104        {
105            FMLRelaunchLog.log(Level.SEVERE, e, "A critical problem occured registering the ASM transformer class %s", transformerClassName);
106        }
107    }
108    @Override
109    public Class<?> findClass(String name) throws ClassNotFoundException
110    {
111        if (invalidClasses.contains(name))
112        {
113            throw new ClassNotFoundException(name);
114        }
115        for (String st : classLoaderExceptions)
116        {
117            if (name.startsWith(st))
118            {
119                return parent.loadClass(name);
120            }
121        }
122
123        if (cachedClasses.containsKey(name))
124        {
125            return cachedClasses.get(name);
126        }
127
128        for (String st : transformerExceptions)
129        {
130            if (name.startsWith(st))
131            {
132                try
133                {
134                    Class<?> cl = super.findClass(name);
135                    cachedClasses.put(name, cl);
136                    return cl;
137                }
138                catch (ClassNotFoundException e)
139                {
140                    invalidClasses.add(name);
141                    throw e;
142                }
143            }
144        }
145
146        try
147        {
148            CodeSigner[] signers = null;
149            String transformedName = transformName(name);
150            String untransformedName = untransformName(name);
151            int lastDot = untransformedName.lastIndexOf('.');
152            String pkgname = lastDot == -1 ? "" : untransformedName.substring(0, lastDot);
153            String fName = untransformedName.replace('.', '/').concat(".class");
154            String pkgPath = pkgname.replace('.', '/');
155            URLConnection urlConnection = findCodeSourceConnectionFor(fName);
156            if (urlConnection instanceof JarURLConnection && lastDot > -1 && !untransformedName.startsWith("net.minecraft."))
157            {
158                JarURLConnection jarUrlConn = (JarURLConnection)urlConnection;
159                JarFile jf = jarUrlConn.getJarFile();
160                if (jf != null && jf.getManifest() != null)
161                {
162                    Manifest mf = jf.getManifest();
163                    JarEntry ent = jf.getJarEntry(fName);
164                    Package pkg = getPackage(pkgname);
165                    getClassBytes(untransformedName);
166                    signers = ent.getCodeSigners();
167                    if (pkg == null)
168                    {
169                        pkg = definePackage(pkgname, mf, jarUrlConn.getJarFileURL());
170                        packageManifests.put(pkg, mf);
171                    }
172                    else
173                    {
174                        if (pkg.isSealed() && !pkg.isSealed(jarUrlConn.getJarFileURL()))
175                        {
176                            FMLLog.severe("The jar file %s is trying to seal already secured path %s", jf.getName(), pkgname);
177                        }
178                        else if (isSealed(pkgname, mf))
179                        {
180                            FMLLog.severe("The jar file %s has a security seal for path %s, but that path is defined and not secure", jf.getName(), pkgname);
181                        }
182                    }
183                }
184            }
185            else if (lastDot > -1 && !untransformedName.startsWith("net.minecraft."))
186            {
187                Package pkg = getPackage(pkgname);
188                if (pkg == null)
189                {
190                    pkg = definePackage(pkgname, null, null, null, null, null, null, null);
191                    packageManifests.put(pkg, EMPTY);
192                }
193                else if (pkg.isSealed())
194                {
195                    FMLLog.severe("The URL %s is defining elements for sealed path %s", urlConnection.getURL(), pkgname);
196                }
197            }
198            byte[] basicClass = getClassBytes(untransformedName);
199            byte[] transformedClass = runTransformers(untransformedName, transformedName, basicClass);
200            Class<?> cl = defineClass(transformedName, transformedClass, 0, transformedClass.length, (urlConnection == null ? null : new CodeSource(urlConnection.getURL(), signers)));
201            cachedClasses.put(transformedName, cl);
202            return cl;
203        }
204        catch (Throwable e)
205        {
206            invalidClasses.add(name);
207            if (DEBUG_CLASSLOADING)
208            {
209                FMLLog.log(Level.FINEST, e, "Exception encountered attempting classloading of %s", name);
210            }
211            throw new ClassNotFoundException(name, e);
212        }
213    }
214
215    private String untransformName(String name)
216    {
217        if (renameTransformer != null)
218        {
219            return renameTransformer.unmapClassName(name);
220        }
221        else
222        {
223            return name;
224        }
225    }
226
227    private String transformName(String name)
228    {
229        if (renameTransformer != null)
230        {
231            return renameTransformer.remapClassName(name);
232        }
233        else
234        {
235            return name;
236        }
237    }
238
239    private boolean isSealed(String path, Manifest man)
240    {
241        Attributes attr = man.getAttributes(path);
242        String sealed = null;
243        if (attr != null) {
244            sealed = attr.getValue(Name.SEALED);
245        }
246        if (sealed == null) {
247            if ((attr = man.getMainAttributes()) != null) {
248                sealed = attr.getValue(Name.SEALED);
249            }
250        }
251        return "true".equalsIgnoreCase(sealed);
252    }
253
254    private URLConnection findCodeSourceConnectionFor(String name)
255    {
256        URL res = findResource(name);
257        if (res != null)
258        {
259            try
260            {
261                return res.openConnection();
262            }
263            catch (IOException e)
264            {
265                throw new RuntimeException(e);
266            }
267        }
268        else
269        {
270            return null;
271        }
272    }
273
274    private byte[] runTransformers(String name, String transformedName, byte[] basicClass)
275    {
276        if (DEBUG_CLASSLOADING_FINER)
277        {
278            FMLLog.finest("Beginning transform of %s (%s) Start Length: %d", name, transformedName, (basicClass == null ? 0 : basicClass.length));
279            for (IClassTransformer transformer : transformers)
280            {
281                String transName = transformer.getClass().getName();
282                FMLLog.finest("Before Transformer %s: %d", transName, (basicClass == null ? 0 : basicClass.length));
283                basicClass = transformer.transform(name, transformedName, basicClass);
284                FMLLog.finest("After  Transformer %s: %d", transName, (basicClass == null ? 0 : basicClass.length));
285            }
286            FMLLog.finest("Ending transform of %s (%s) Start Length: %d", name, transformedName, (basicClass == null ? 0 : basicClass.length));
287        }
288        else
289        {
290            for (IClassTransformer transformer : transformers)
291            {
292                basicClass = transformer.transform(name, transformedName, basicClass);
293            }
294        }
295        return basicClass;
296    }
297
298    @Override
299    public void addURL(URL url)
300    {
301        super.addURL(url);
302        sources.add(url);
303    }
304
305    public List<URL> getSources()
306    {
307        return sources;
308    }
309
310
311    private byte[] readFully(InputStream stream)
312    {
313        try
314        {
315            byte[] buf = loadBuffer.get();
316            if (buf == null)
317            {
318                loadBuffer.set(new byte[1 << 12]);
319                buf = loadBuffer.get();
320            }
321
322            int r, totalLength = 0;
323            while ((r = stream.read(buf, totalLength, buf.length - totalLength)) != -1)
324            {
325                totalLength += r;
326                if (totalLength >= buf.length - 1)
327                {
328                    byte[] oldbuf = buf;
329                    buf = new byte[ oldbuf.length + (1 << 12 )];
330                    System.arraycopy(oldbuf, 0, buf, 0, oldbuf.length);
331                }
332            }
333
334            byte[] result = new byte[totalLength];
335            System.arraycopy(buf, 0, result, 0, totalLength);
336            return result;
337        }
338        catch (Throwable t)
339        {
340            FMLRelaunchLog.log(Level.WARNING, t, "Problem loading class");
341            return new byte[0];
342        }
343    }
344
345    public List<IClassTransformer> getTransformers()
346    {
347        return Collections.unmodifiableList(transformers);
348    }
349
350    private void addClassLoaderExclusion(String toExclude)
351    {
352        classLoaderExceptions.add(toExclude);
353    }
354
355    void addTransformerExclusion(String toExclude)
356    {
357        transformerExceptions.add(toExclude);
358    }
359
360    public byte[] getClassBytes(String name) throws IOException
361    {
362        if (name.indexOf('.') == -1)
363        {
364            for (String res : RESERVED)
365            {
366                if (name.toUpperCase(Locale.ENGLISH).startsWith(res))
367                {
368                    byte[] data = getClassBytes("_" + name);
369                    if (data != null)
370                    {
371                        return data;
372                    }
373                }
374            }
375        }
376
377        InputStream classStream = null;
378        try
379        {
380            URL classResource = findResource(name.replace('.', '/').concat(".class"));
381            if (classResource == null)
382            {
383                if (DEBUG_CLASSLOADING)
384                {
385                    FMLLog.finest("Failed to find class resource %s", name.replace('.', '/').concat(".class"));
386                }
387                return null;
388            }
389            classStream = classResource.openStream();
390            if (DEBUG_CLASSLOADING)
391            {
392                FMLLog.finest("Loading class %s from resource %s", name, classResource.toString());
393            }
394            return readFully(classStream);
395        }
396        finally
397        {
398            if (classStream != null)
399            {
400                try
401                {
402                    classStream.close();
403                }
404                catch (IOException e)
405                {
406                    // Swallow the close exception
407                }
408            }
409        }
410    }
411}