    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}