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