/*
* This file is part of Mixin, licensed under the MIT License (MIT).
*
* Copyright (c) SpongePowered <https://www.spongepowered.org>
* 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;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.launch.MixinInitialisationError;
import org.spongepowered.asm.lib.tree.ClassNode;
import org.spongepowered.asm.mixin.MixinEnvironment;
import org.spongepowered.asm.mixin.MixinEnvironment.CompatibilityLevel;
import org.spongepowered.asm.mixin.MixinEnvironment.Option;
import org.spongepowered.asm.mixin.MixinEnvironment.Phase;
import org.spongepowered.asm.mixin.extensibility.IMixinConfig;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.InjectionPoint;
import org.spongepowered.asm.mixin.refmap.ReferenceMapper;
import org.spongepowered.asm.mixin.transformer.throwables.InvalidMixinException;
import org.spongepowered.asm.util.VersionNumber;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import net.minecraft.launchwrapper.Launch;
/**
* Mixin configuration bundle
*/
final class MixinConfig implements Comparable<MixinConfig>, IMixinConfig {
/**
* Wrapper for injection options
*/
static class InjectorOptions {
@SerializedName("defaultRequire")
int defaultRequireValue = 0;
@SerializedName("defaultGroup")
String defaultGroup = "default";
@SerializedName("injectionPoints")
List<String> injectionPoints;
}
/**
* Callback listener for certain mixin init steps
*/
interface IListener {
/**
* Called when a mixin has been successfully prepared
*
* @param mixin mixin which was prepared
*/
public abstract void onPrepare(MixinInfo mixin);
/**
* Called when a mixin has completed post-initialisation
*
* @param mixin mixin which completed postinit
*/
public abstract void onInit(MixinInfo mixin);
}
/**
* Global order of mixin configs, used to determine ordering between configs
* with equivalent priority
*/
private static int configOrder = 0;
/**
* Global list of mixin classes, so we can skip any duplicates
*/
private static final Set<String> globalMixinList = new HashSet<String>();
/**
* Log even more things
*/
private final Logger logger = LogManager.getLogger("mixin");
/**
* Map of mixin target classes to mixin infos
*/
private final transient Map<String, List<MixinInfo>> mixinMapping = new HashMap<String, List<MixinInfo>>();
/**
* Targets for this configuration which haven't been mixed yet
*/
private final transient Set<String> unhandledTargets = new HashSet<String>();
/**
* All mixins loaded by this config
*/
private final transient List<MixinInfo> mixins = new ArrayList<MixinInfo>();
/**
* Marshal
*/
private transient Config handle;
/**
* Target selector, eg. @env(DEFAULT)
*/
@SerializedName("target")
private String selector;
/**
* Minimum version of the mixin subsystem required to correctly apply mixins
* in this configuration.
*/
@SerializedName("minVersion")
private String version;
/**
* Minimum compatibility level required for mixins in this set
*/
@SerializedName("compatibilityLevel")
private String compatibility;
/**
* Determines whether failures in this mixin config are considered terminal
* errors. Use this setting to indicate that failing to apply a mixin in
* this config is a critical error and should cause the game to shutdown.
*/
@SerializedName("required")
private boolean required;
/**
* Configuration priority
*/
@SerializedName("priority")
private int priority = IMixinConfig.DEFAULT_PRIORITY;
/**
* Default mixin priority. By default, mixins get a priority of
* {@link IMixinConfig#DEFAULT_PRIORITY DEFAULT_PRIORITY} unless a different
* value is specified in the annotation. This setting allows the base
* priority for all mixins in this config to be set to an alternate value.
*/
@SerializedName("mixinPriority")
private int mixinPriority = IMixinConfig.DEFAULT_PRIORITY;
/**
* Package containing all mixins. This package will be monitored by the
* transformer so that we can explode if some dummy tries to reference a
* mixin class directly.
*/
@SerializedName("package")
private String mixinPackage;
/**
* Mixin classes to load, mixinPackage will be prepended
*/
@SerializedName("mixins")
private List<String> mixinClasses;
/**
* Mixin classes to load ONLY on client, mixinPackage will be prepended
*/
@SerializedName("client")
private List<String> mixinClassesClient;
/**
* Mixin classes to load ONLY on dedicated server, mixinPackage will be
* prepended
*/
@SerializedName("server")
private List<String> mixinClassesServer;
/**
* True to set the sourceFile property when applying mixins
*/
@SerializedName("setSourceFile")
private boolean setSourceFile = false;
/**
* The path to the reference map resource to use for this configuration
*/
@SerializedName("refmap")
private String refMapperConfig;
/**
* True to output "mixing in" messages at INFO level rather than DEBUG
*/
@SerializedName("verbose")
private boolean verboseLogging;
/**
* Intrinsic order (for sorting configurations with identical priority)
*/
private final transient int order = MixinConfig.configOrder++;
private final transient List<IListener> listeners = new ArrayList<IListener>();
// /**
// * Phase selector
// */
// private transient List<Selector> selectors;
/**
* Parent environment
*/
private transient MixinEnvironment env;
/**
* Name of the file this config was initialised from
*/
private transient String name;
/**
* Name of the {@link IMixinConfigPlugin} to hook onto this MixinConfig
*/
@SerializedName("plugin")
private String pluginClassName;
/**
* Injector options
*/
@SerializedName("injectors")
private InjectorOptions injectorOptions = new InjectorOptions();
/**
* Config plugin, if supplied
*/
private transient IMixinConfigPlugin plugin;
/**
* Reference mapper for injectors
*/
private transient ReferenceMapper refMapper;
/**
* Keep track of initialisation state
*/
private transient boolean prepared = false;
/**
* Track whether this mixin has been evaluated for selection yet
*/
private transient boolean visited = false;
/**
* Spawn via GSON, no public ctor for you
*/
private MixinConfig() {}
/**
* Called immediately after deserialisation
*
* @param name Mixin config name
* @param fallbackEnvironment Fallback environment if not specified in
* config
* @return true if the config was successfully initialised and should be
* returned, or false if initialisation failed and the config should
* be discarded
*/
private boolean onLoad(String name, MixinEnvironment fallbackEnvironment) {
this.name = name;
this.env = this.parseSelector(this.selector, fallbackEnvironment);
this.required &= !this.env.getOption(Option.IGNORE_REQUIRED);
this.initCompatibilityLevel();
this.initInjectionPoints();
return this.checkVersion();
}
@SuppressWarnings("deprecation")
private void initCompatibilityLevel() {
if (this.compatibility == null) {
return;
}
CompatibilityLevel level = CompatibilityLevel.valueOf(this.compatibility.trim().toUpperCase());
CompatibilityLevel current = MixinEnvironment.getCompatibilityLevel();
if (level == current) {
return;
}
// Current level is higher than required but too new to support it
if (current.isAtLeast(level)) {
if (!current.canSupport(level)) {
throw new MixinInitialisationError("Mixin config " + this.name + " requires compatibility level " + level + " which is too old");
}
}
// Current level is lower than required but current level prohibits elevation
if (!current.canElevateTo(level)) {
throw new MixinInitialisationError("Mixin config " + this.name + " requires compatibility level " + level + " which is prohibited by "
+ current);
}
MixinEnvironment.setCompatibilityLevel(level);
}
// AMS - temp
private MixinEnvironment parseSelector(String target, MixinEnvironment fallbackEnvironment) {
if (target != null) {
String[] selectors = target.split("[&\\| ]");
for (String sel : selectors) {
sel = sel.trim();
Pattern environmentSelector = Pattern.compile("^@env(?:ironment)?\\(([A-Z]+)\\)$");
Matcher environmentSelectorMatcher = environmentSelector.matcher(sel);
if (environmentSelectorMatcher.matches()) {
// only parse first env selector
return MixinEnvironment.getEnvironment(Phase.forName(environmentSelectorMatcher.group(1)));
}
}
Phase phase = Phase.forName(target);
if (phase != null) {
return MixinEnvironment.getEnvironment(phase);
}
}
return fallbackEnvironment;
}
@SuppressWarnings("unchecked")
private void initInjectionPoints() {
if (this.injectorOptions.injectionPoints == null) {
return;
}
for (String injectionPoint : this.injectorOptions.injectionPoints) {
try {
Class<?> injectionPointClass = Class.forName(injectionPoint, true, Launch.classLoader);
if (InjectionPoint.class.isAssignableFrom(injectionPointClass)) {
InjectionPoint.register((Class<? extends InjectionPoint>)injectionPointClass);
} else {
this.logger.error("Unable to register injection point {} for {}, class must extend InjectionPoint", injectionPointClass, this);
}
} catch (Throwable th) {
this.logger.catching(th);
}
}
}
private boolean checkVersion() throws MixinInitialisationError {
VersionNumber minVersion = VersionNumber.parse(this.version);
VersionNumber curVersion = VersionNumber.parse(this.env.getVersion());
if (minVersion.compareTo(curVersion) > 0) {
this.logger.warn("Mixin config {} requires mixin subsystem version {} but {} was found. The mixin config will not be applied.",
this.name, minVersion, curVersion);
if (this.required) {
throw new MixinInitialisationError("Required mixin config " + this.name + " requires mixin subsystem version " + minVersion);
}
return false;
}
return true;
}
/**
* Add a new listener
*
* @param listener listener to add
*/
void addListener(IListener listener) {
this.listeners.add(listener);
}
/**
* Initialise the config once it's selected
*/
void onSelect() {
if (this.pluginClassName != null) {
try {
Class<?> pluginClass = Class.forName(this.pluginClassName, true, Launch.classLoader);
this.plugin = (IMixinConfigPlugin)pluginClass.newInstance();
if (this.plugin != null) {
this.plugin.onLoad(this.mixinPackage);
}
} catch (Throwable th) {
th.printStackTrace();
this.plugin = null;
}
}
if (!this.mixinPackage.endsWith(".")) {
this.mixinPackage += ".";
}
if (this.refMapperConfig == null) {
if (this.plugin != null) {
this.refMapperConfig = this.plugin.getRefMapperConfig();
}
if (this.refMapperConfig == null) {
this.refMapperConfig = ReferenceMapper.DEFAULT_RESOURCE;
}
}
this.refMapper = ReferenceMapper.read(this.refMapperConfig);
this.verboseLogging |= this.env.getOption(Option.DEBUG_VERBOSE);
}
/**
* <p>Initialisation routine. It's important that we call this routine as
* late as possible. In general we want to call it on the first call to
* transform() in the parent transformer. At the very least we want to be
* called <em>after</em> all the transformers for the current environment
* have been spawned, because we will run the mixin bytecode through the
* transformer chain and naturally we want this to happen at a point when we
* can be reasonably sure that all transfomers have loaded.</p>
*
* <p>For this reason we will invoke the initialisation on the first call to
* either the <em>hasMixinsFor()</em> or <em>getMixinsFor()</em> methods.
* </p>
*/
void prepare() {
if (this.prepared) {
return;
}
this.prepared = true;
this.prepareMixins(this.mixinClasses, false);
switch (this.env.getSide()) {
case CLIENT:
this.prepareMixins(this.mixinClassesClient, false);
break;
case SERVER:
this.prepareMixins(this.mixinClassesServer, false);
break;
case UNKNOWN:
//$FALL-THROUGH$
default:
this.logger.warn("Mixin environment was unable to detect the current side, sided mixins will not be applied");
break;
}
}
void postInitialise() {
if (this.plugin != null) {
List<String> pluginMixins = this.plugin.getMixins();
this.prepareMixins(pluginMixins, true);
}
for (Iterator<MixinInfo> iter = this.mixins.iterator(); iter.hasNext();) {
MixinInfo mixin = iter.next();
try {
mixin.validate();
for (IListener listener : this.listeners) {
listener.onInit(mixin);
}
} catch (InvalidMixinException ex) {
this.logger.error(ex.getMixin() + ": " + ex.getMessage(), ex);
this.removeMixin(mixin);
iter.remove();
} catch (Exception ex) {
this.logger.error(ex.getMessage(), ex);
this.removeMixin(mixin);
iter.remove();
}
}
}
private void removeMixin(MixinInfo remove) {
for (List<MixinInfo> mixinsFor : this.mixinMapping.values()) {
for (Iterator<MixinInfo> iter = mixinsFor.iterator(); iter.hasNext();) {
if (remove == iter.next()) {
iter.remove();
}
}
}
}
private void prepareMixins(List<String> mixinClasses, boolean suppressPlugin) {
if (mixinClasses == null) {
return;
}
for (String mixinClass : mixinClasses) {
String fqMixinClass = this.mixinPackage + mixinClass;
if (mixinClass == null || MixinConfig.globalMixinList.contains(fqMixinClass)) {
continue;
}
MixinInfo mixin = null;
try {
mixin = new MixinInfo(this, mixinClass, true, this.plugin, suppressPlugin);
if (mixin.getTargetClasses().size() > 0) {
MixinConfig.globalMixinList.add(fqMixinClass);
for (String targetClass : mixin.getTargetClasses()) {
String targetClassName = targetClass.replace('/', '.');
this.mixinsFor(targetClassName).add(mixin);
this.unhandledTargets.add(targetClassName);
}
for (IListener listener : this.listeners) {
listener.onPrepare(mixin);
}
this.mixins.add(mixin);
}
} catch (InvalidMixinException ex) {
if (this.required) {
throw ex;
}
this.logger.error(ex.getMessage(), ex);
} catch (Exception ex) {
if (this.required) {
throw new InvalidMixinException(mixin, "Error initialising mixin " + mixin + " - " + ex.getClass() + ": " + ex.getMessage(), ex);
}
this.logger.error(ex.getMessage(), ex);
}
}
}
void postApply(String transformedName, ClassNode targetClass) {
this.unhandledTargets.remove(transformedName);
}
/**
* Get marshalling handle
*/
public Config getHandle() {
if (this.handle == null) {
this.handle = new Config(this);
}
return this.handle;
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.transformer.IMixinConfig#isRequired()
*/
@Override
public boolean isRequired() {
return this.required;
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.extensibility.IMixinConfig
* #getEnvironment()
*/
@Override
public MixinEnvironment getEnvironment() {
return this.env;
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.transformer.IMixinConfig#getName()
*/
@Override
public String getName() {
return this.name;
}
/**
* Get the package containing all mixin classes
*/
@Override
public String getMixinPackage() {
return this.mixinPackage;
}
/**
* Get the priority
*/
@Override
public int getPriority() {
return this.priority;
}
/**
* Get the default priority for mixins in this config. Values specified in
* the mixin annotation still override this value
*/
public int getDefaultMixinPriority() {
return this.mixinPriority;
}
/**
* Get the defined value for the {@link Inject#require} parameter on
* injectors defined in mixins in this configuration.
*
* @return default require value
*/
public int getDefaultRequiredInjections() {
return this.injectorOptions.defaultRequireValue;
}
/**
* Get the defined injector group for injectors
*
* @return default group name
*/
public String getDefaultInjectorGroup() {
String defaultGroup = this.injectorOptions.defaultGroup;
return defaultGroup != null && !defaultGroup.isEmpty() ? defaultGroup : "default";
}
// AMS - temp
public boolean select(MixinEnvironment environment) {
this.visited = true;
return this.env == environment;
}
// AMS - temp
boolean isVisited() {
return this.visited;
}
/**
* Get the number of mixins in this config, for debug logging
*
* @return total enumerated mixins in set
*/
int getDeclaredMixinCount() {
return MixinConfig.getCollectionSize(this.mixinClasses, this.mixinClassesClient, this.mixinClassesServer);
}
/**
* Get the number of mixins actually initialised, for debug logging
*
* @return total enumerated mixins in set
*/
int getMixinCount() {
return this.mixins.size();
}
/**
* Get the list of mixin classes we will be applying
*/
public List<String> getClasses() {
return Collections.<String>unmodifiableList(this.mixinClasses);
}
/**
* Get whether to propogate the source file attribute from a mixin onto the
* target class
*/
public boolean shouldSetSourceFile() {
return this.setSourceFile;
}
/**
* Get the reference remapper for injectors
*/
public ReferenceMapper getReferenceMapper() {
if (this.env.getOption(Option.DISABLE_REFMAP)) {
return ReferenceMapper.DEFAULT_MAPPER;
}
this.refMapper.setContext(this.env.getRefmapObfuscationContext());
return this.refMapper;
}
String remapClassName(String className, String reference) {
// String remapped = this.plugin != null ? this.plugin.remap(className, reference) : null;
// if (remapped != null) {
// return remapped;
// }
return this.getReferenceMapper().remap(className, reference);
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.transformer.IMixinConfig#getPlugin()
*/
@Override
public IMixinConfigPlugin getPlugin() {
return this.plugin;
}
/* (non-Javadoc)
* @see org.spongepowered.asm.mixin.transformer.IMixinConfig#getTargets()
*/
@Override
public Set<String> getTargets() {
return Collections.<String>unmodifiableSet(this.mixinMapping.keySet());
}
/**
* Get targets for this configuration
*/
public Set<String> getUnhandledTargets() {
return Collections.<String>unmodifiableSet(this.unhandledTargets);
}
/**
* Get the logging level for this config
*/
public Level getLoggingLevel() {
return this.verboseLogging ? Level.INFO : Level.DEBUG;
}
/**
* Get whether this config's package matches the supplied class name
*
* @param className Class name to check
* @return True if the specified class name is in this config's mixin
* package
*/
public boolean packageMatch(String className) {
return className.startsWith(this.mixinPackage);
}
/**
* Check whether this configuration bundle has a mixin for the specified
* class
*
* @param targetClass target class
* @return true if this bundle contains any mixins for the specified target
*/
public boolean hasMixinsFor(String targetClass) {
return this.mixinMapping.containsKey(targetClass);
}
/**
* Get mixins for the specified target class
*
* @param targetClass target class
* @return mixins for the specified target
*/
public List<MixinInfo> getMixinsFor(String targetClass) {
return this.mixinsFor(targetClass);
}
private List<MixinInfo> mixinsFor(String targetClass) {
List<MixinInfo> mixins = this.mixinMapping.get(targetClass);
if (mixins == null) {
mixins = new ArrayList<MixinInfo>();
this.mixinMapping.put(targetClass, mixins);
}
return mixins;
}
/**
* Updates a mixin with new bytecode
*
* @param mixinClass Name of the mixin class
* @param bytes New bytecode
* @return List of classes that need to be updated
*/
public List<String> reloadMixin(String mixinClass, byte[] bytes) {
for (Iterator<MixinInfo> iter = this.mixins.iterator(); iter.hasNext();) {
MixinInfo mixin = iter.next();
if (mixin.getClassName().equals(mixinClass)) {
mixin.reloadMixin(bytes);
return mixin.getTargetClasses();
}
}
return Collections.<String>emptyList();
}
@Override
public String toString() {
return this.name;
}
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(MixinConfig other) {
if (other == null) {
return 0;
}
if (other.priority == this.priority) {
return this.order - other.order;
}
return (this.priority - other.priority);
}
/**
* Factory method, creates a new mixin configuration bundle from the
* specified configFile, which must be accessible on the classpath
*
* @param configFile configuration file to load
* @param outer fallback environment
* @return new Config
*/
static Config create(String configFile, MixinEnvironment outer) {
try {
MixinConfig config = new Gson().fromJson(new InputStreamReader(Launch.classLoader.getResourceAsStream(configFile)), MixinConfig.class);
if (config.onLoad(configFile, outer)) {
return config.getHandle();
}
return null;
} catch (Exception ex) {
ex.printStackTrace();
throw new IllegalArgumentException(String.format("The specified resource '%s' was invalid or could not be read", configFile), ex);
}
}
private static int getCollectionSize(Collection<?>... collections) {
int total = 0;
for (Collection<?> collection : collections) {
if (collection != null) {
total += collection.size();
}
}
return total;
}
}