diff --git a/gradle.properties b/gradle.properties index 95a63153a..1bf149327 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,5 +4,5 @@ packaging=jar description=Mixin url=https://www.spongepowered.org organization=SpongePowered -buildVersion=0.5.8 +buildVersion=0.5.9 buildType=SNAPSHOT diff --git a/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java b/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java index 9cc14aa3c..ff5ecf739 100644 --- a/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java +++ b/src/main/java/org/spongepowered/asm/launch/MixinBootstrap.java @@ -60,13 +60,7 @@ public abstract class MixinBootstrap { /** * Subsystem version */ - public static final String VERSION = "0.5.8"; - - /** - * Blackboard key where the subsystem version will be stored to indicate - * successful bootstrap - */ -// public static final String INIT_KEY = Blackboard.Keys.INIT; + public static final String VERSION = "0.5.9"; // Consts private static final String LAUNCH_PACKAGE = "org.spongepowered.asm.launch."; diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java index ac63fe85f..f1d41c96a 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/MixinInfo.java @@ -25,7 +25,6 @@ package org.spongepowered.asm.mixin.transformer; import java.io.IOException; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -58,14 +57,15 @@ import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException; import org.spongepowered.asm.mixin.transformer.throwables.MixinReloadException; +import org.spongepowered.asm.mixin.transformer.throwables.MixinTargetAlreadyLoadedException; import org.spongepowered.asm.util.ASMHelper; +import org.spongepowered.asm.util.launchwrapper.LaunchClassLoaderUtil; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.collect.Lists; import net.minecraft.launchwrapper.Launch; -import net.minecraft.launchwrapper.LaunchClassLoader; /** * Runtime information bundle about a mixin @@ -531,14 +531,17 @@ MixinPreProcessorStandard createPreProcessor(ClassNode classNode) { } } + /** + * Class loader utility + */ + private static final LaunchClassLoaderUtil classLoaderUtil = LaunchClassLoaderUtil.forClassLoader(Launch.classLoader); + /** * Global order of mixin infos, used to determine ordering between mixins * with equivalent priority */ static int mixinOrder = 0; - static final Set invalidClasses = MixinInfo.$getInvalidClassesSet(); - /** * Logger */ @@ -637,6 +640,8 @@ MixinPreProcessorStandard createPreProcessor(ClassNode classNode) { this.pendingState = new State(mixinBytes); this.info = this.pendingState.getClassInfo(); this.type = this.initType(); + } catch (InvalidMixinException ex) { + throw ex; } catch (Exception ex) { throw new InvalidMixinException(this, ex); } @@ -646,6 +651,8 @@ MixinPreProcessorStandard createPreProcessor(ClassNode classNode) { this.priority = this.readPriority(this.pendingState.getClassNode()); this.targetClasses = this.readTargetClasses(this.pendingState.getClassNode(), suppressPlugin); this.targetClassNames = Collections.unmodifiableList(Lists.transform(this.targetClasses, Functions.toStringFunction())); + } catch (InvalidMixinException ex) { + throw ex; } catch (Exception ex) { throw new InvalidMixinException(this, ex); } @@ -725,8 +732,16 @@ public String apply(String input) { * Reads a target list into the outTargets list */ private void readTargets(Collection outTargets, Collection inTargets, boolean suppressPlugin, boolean checkPublic) { - for (String targetName : inTargets) { - targetName = targetName.replace('/', '.'); + for (String targetRef : inTargets) { + String targetName = targetRef.replace('/', '.'); + if (MixinInfo.classLoaderUtil.isClassLoaded(targetName)) { + String message = String.format("Critical problem: %s target %s was already transformed.", this, targetName); + if (this.parent.isRequired()) { + throw new MixinTargetAlreadyLoadedException(this, message); + } + this.logger.error(message); + } + if (this.plugin == null || suppressPlugin || this.plugin.shouldApplyMixin(targetName, this.className)) { ClassInfo targetInfo = this.getTarget(targetName, checkPublic); if (targetInfo != null && !outTargets.contains(targetInfo)) { @@ -740,12 +755,12 @@ private void readTargets(Collection outTargets, Collection in private ClassInfo getTarget(String targetName, boolean checkPublic) throws InvalidMixinException { ClassInfo targetInfo = ClassInfo.forName(targetName); if (targetInfo == null) { - this.handleTargetError("@Mixin target " + targetName + " was not found " + this); + this.handleTargetError(String.format("@Mixin target %s was not found %s", targetName, this)); return null; } this.type.validateTarget(targetName, targetInfo); if (checkPublic && targetInfo.isPublic()) { - this.handleTargetError("@Mixin target " + targetName + " is public in " + this + " and should be specified in value"); + this.handleTargetError(String.format("@Mixin target %s is public in %s and should be specified in value", targetName, this)); } return targetInfo; } @@ -957,9 +972,7 @@ private byte[] loadMixinClass(String mixinClassName, boolean runTransformers) th // Inject the mixin class name into the LaunchClassLoader's invalid // classes set so that any classes referencing the mixin directly will // cause the game to crash - if (MixinInfo.invalidClasses != null) { - MixinInfo.invalidClasses.add(mixinClassName); - } + MixinInfo.classLoaderUtil.registerInvalidClass(mixinClassName); return mixinBytes; } @@ -1018,17 +1031,5 @@ public void postApply(String transformedName, ClassNode targetClass) { public String toString() { return String.format("%s:%s", this.parent.getName(), this.name); } - - @SuppressWarnings("unchecked") - private static Set $getInvalidClassesSet() { - try { - Field invalidClasses = LaunchClassLoader.class.getDeclaredField("invalidClasses"); - invalidClasses.setAccessible(true); - return (Set)invalidClasses.get(Launch.classLoader); - } catch (Exception ex) { - ex.printStackTrace(); - } - return null; - } } diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/TreeInfo.java b/src/main/java/org/spongepowered/asm/mixin/transformer/TreeInfo.java index 13761a013..91a8b032f 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/TreeInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/TreeInfo.java @@ -35,6 +35,7 @@ import org.spongepowered.asm.lib.tree.ClassNode; import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.transformer.MixinTransformer.ReEntranceState; +import org.spongepowered.asm.util.launchwrapper.LaunchClassLoaderUtil; import net.minecraft.launchwrapper.IClassTransformer; import net.minecraft.launchwrapper.Launch; @@ -136,6 +137,10 @@ private static byte[] getClassBytes(String name, String transformedName) throws * except the excluded transformers */ private static byte[] applyTransformers(String name, String transformedName, byte[] basicClass) { + if (LaunchClassLoaderUtil.forClassLoader(Launch.classLoader).isClassExcluded(name, transformedName)) { + return basicClass; + } + MixinEnvironment environment = MixinEnvironment.getCurrentEnvironment(); for (IClassTransformer transformer : environment.getTransformers()) { diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/throwables/MixinTargetAlreadyLoadedException.java b/src/main/java/org/spongepowered/asm/mixin/transformer/throwables/MixinTargetAlreadyLoadedException.java new file mode 100644 index 000000000..70780f297 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/throwables/MixinTargetAlreadyLoadedException.java @@ -0,0 +1,48 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.asm.mixin.transformer.throwables; + +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +/** + * Exception thrown when a specified mixin target was already classloaded. + */ +public class MixinTargetAlreadyLoadedException extends InvalidMixinException { + + private static final long serialVersionUID = 1L; + + public MixinTargetAlreadyLoadedException(IMixinInfo mixin, String message) { + super(mixin, message); + } + + public MixinTargetAlreadyLoadedException(IMixinInfo mixin, Throwable cause) { + super(mixin, cause); + } + + public MixinTargetAlreadyLoadedException(IMixinInfo mixin, String message, Throwable cause) { + super(mixin, message, cause); + } + +} diff --git a/src/main/java/org/spongepowered/asm/util/launchwrapper/LaunchClassLoaderUtil.java b/src/main/java/org/spongepowered/asm/util/launchwrapper/LaunchClassLoaderUtil.java new file mode 100644 index 000000000..cec9bcaf1 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/util/launchwrapper/LaunchClassLoaderUtil.java @@ -0,0 +1,192 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.asm.util.launchwrapper; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import net.minecraft.launchwrapper.LaunchClassLoader; + +/** + * Utility class for reflecting into {@link LaunchClassLoader}. We do not + * write anything of the classloader fields, but we need to be able to read + * them to perform some validation tasks, and insert entries for mixin "classes" + * into the invalid classes set. + */ +public final class LaunchClassLoaderUtil { + + /** + * ClassLoader -> util mapping + */ + private static final Map utils = new HashMap(); + + /** + * ClassLoader for this util + */ + private final LaunchClassLoader classLoader; + + // Reflected fields + private Map> cachedClasses; + private final Set invalidClasses; + private final Set classLoaderExceptions; + private final Set transformerExceptions; + + /** + * Singleton, use factory to get an instance + * + * @param classLoader class loader + */ + private LaunchClassLoaderUtil(LaunchClassLoader classLoader) { + this.classLoader = classLoader; + this.cachedClasses = LaunchClassLoaderUtil.>>getField(classLoader, "cachedClasses"); + this.invalidClasses = LaunchClassLoaderUtil.>getField(classLoader, "invalidClasses"); + this.classLoaderExceptions = LaunchClassLoaderUtil.>getField(classLoader, "classLoaderExceptions"); + this.transformerExceptions = LaunchClassLoaderUtil.>getField(classLoader, "transformerExceptions"); + } + + /** + * Get the classloader + */ + public LaunchClassLoader getClassLoader() { + return this.classLoader; + } + + /** + * Get all loaded class names from the cache + */ + public Set getLoadedClasses() { + return this.getLoadedClasses(null); + } + + /** + * Get the names of loaded classes from the cache, filter using the supplied + * filter string + * + * @param filter filter string or null + * @return set of class names + */ + public Set getLoadedClasses(String filter) { + Set loadedClasses = new HashSet(); + for (String className : this.cachedClasses.keySet()) { + if (filter == null || className.startsWith(filter)) { + loadedClasses.add(className); + } + } + return loadedClasses; + } + + /** + * Get whether a class name exists in the cache (indicating it was loaded + * via the inner loader + * + * @param name class name + * @return true if the class name exists in the cache + */ + public boolean isClassLoaded(String name) { + return this.cachedClasses != null && this.cachedClasses.containsKey(name); + } + + /** + * Get whether the specified name or transformedName exist in either of the + * exclusion lists + * + * @param name class name + * @param transformedName transformed class name + * @return true if either exclusion list contains either of the names + */ + public boolean isClassExcluded(String name, String transformedName) { + for (final String exception : this.getClassLoaderExceptions()) { + if (transformedName.startsWith(exception) || name.startsWith(exception)) { + return true; + } + } + + for (final String exception : this.getTransformerExceptions()) { + if (transformedName.startsWith(exception) || name.startsWith(exception)) { + return true; + } + } + + return false; + } + + /** + * Stuff a class name directly into the invalidClasses set, this prevents + * the loader from classloading the named class. This is used by the mixin + * processor to prevent classloading of mixin classes + * + * @param name class name + */ + public void registerInvalidClass(String name) { + if (this.invalidClasses != null) { + this.invalidClasses.add(name); + } + } + + public Set getClassLoaderExceptions() { + if (this.classLoaderExceptions != null) { + return this.classLoaderExceptions; + } + return Collections.emptySet(); + } + + public Set getTransformerExceptions() { + if (this.transformerExceptions != null) { + return this.transformerExceptions; + } + return Collections.emptySet(); + } + + @SuppressWarnings("unchecked") + private static T getField(LaunchClassLoader classLoader, String fieldName) { + try { + Field field = LaunchClassLoader.class.getDeclaredField(fieldName); + field.setAccessible(true); + return (T)field.get(classLoader); + } catch (Exception ex) { + ex.printStackTrace(); + } + return null; + } + + /** + * Get the utility class for the supplied classloader + * + * @param classLoader classLoader to fetch utility wrapper for + * @return utility wrapper + */ + public static LaunchClassLoaderUtil forClassLoader(LaunchClassLoader classLoader) { + LaunchClassLoaderUtil util = LaunchClassLoaderUtil.utils.get(classLoader); + if (util == null) { + util = new LaunchClassLoaderUtil(classLoader); + LaunchClassLoaderUtil.utils.put(classLoader, util); + } + return util; + } +}