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