/*
* 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.injection.struct;
import org.spongepowered.asm.lib.Type;
import org.spongepowered.asm.lib.tree.AbstractInsnNode;
import org.spongepowered.asm.lib.tree.FieldInsnNode;
import org.spongepowered.asm.lib.tree.MethodInsnNode;
import org.spongepowered.asm.mixin.refmap.IMixinContext;
import org.spongepowered.asm.mixin.refmap.ReferenceMapper;
import org.spongepowered.asm.mixin.throwables.MixinException;
import org.spongepowered.asm.obfuscation.mapping.IMapping;
import org.spongepowered.asm.obfuscation.mapping.common.MappingField;
import org.spongepowered.asm.obfuscation.mapping.common.MappingMethod;
import org.spongepowered.asm.util.SignaturePrinter;
import com.google.common.base.Strings;
/**
* <p>Information bundle about a member (method or field) parsed from a String
* token in another annotation, this is used where target members need to be
* specified as Strings in order to parse the String representation to something
* useful.</p>
*
* <p>Some examples:</p>
* <blockquote><pre>
* // references a method or field called func_1234_a, if there are multiple
* // members with the same signature, matches the first occurrence
* func_1234_a
*
* // references a method or field called func_1234_a, if there are multiple
* // members with the same signature, matches all occurrences
* func_1234_a*
*
* // references a method called func_1234_a which takes 3 ints and returns
* // a bool
* func_1234_a(III)Z
*
* // references a field called field_5678_z which is a String
* field_5678_z:Ljava/lang/String;
*
* // references a ctor which takes a single String argument
* <init>(Ljava/lang/String;)V
*
* // references a method called func_1234_a in class foo.bar.Baz
* Lfoo/bar/Baz;func_1234_a
*
* // references a field called field_5678_z in class com.example.Dave
* Lcom/example/Dave;field_5678_z
*
* // references a method called func_1234_a in class foo.bar.Baz which takes
* // three doubles and returns void
* Lfoo/bar/Baz;func_1234_a(DDD)V
*
* // alternate syntax for the same
* foo.bar.Baz.func_1234_a(DDD)V</pre>
* </blockquote>
*/
public class MemberInfo {
/**
* Member owner in internal form but without L;, can be null
*/
public final String owner;
/**
* Member name, can be null to match any member
*/
public final String name;
/**
* Member descriptor, can be null
*/
public final String desc;
/**
* True to match all matching members, not just the first
*/
public final boolean matchAll;
/**
* Force this member to report as a field
*/
private final boolean forceField;
/**
* The actual String value passed into the {@link #parse} method
*/
private final String unparsed;
/**
* ctor
*
* @param name Member name, must not be null
* @param matchAll true if this info should match all matching references,
* or only the first
*/
public MemberInfo(String name, boolean matchAll) {
this(name, null, null, matchAll);
}
/**
* ctor
*
* @param name Member name, must not be null
* @param owner Member owner, can be null otherwise must be in internal form
* without L;
* @param matchAll true if this info should match all matching references,
* or only the first
*/
public MemberInfo(String name, String owner, boolean matchAll) {
this(name, owner, null, matchAll);
}
/**
* ctor
*
* @param name Member name, must not be null
* @param owner Member owner, can be null otherwise must be in internal form
* without L;
* @param desc Member descriptor, can be null
*/
public MemberInfo(String name, String owner, String desc) {
this(name, owner, desc, false);
}
/**
* ctor
*
* @param name Member name, must not be null
* @param owner Member owner, can be null otherwise must be in internal form
* without L;
* @param desc Member descriptor, can be null
* @param matchAll True to match all matching members, not just the first
*/
public MemberInfo(String name, String owner, String desc, boolean matchAll) {
this(name, owner, desc, matchAll, null);
}
/**
* ctor
*
* @param name Member name, must not be null
* @param owner Member owner, can be null otherwise must be in internal form
* without L;
* @param desc Member descriptor, can be null
* @param matchAll True to match all matching members, not just the first
*/
public MemberInfo(String name, String owner, String desc, boolean matchAll, String unparsed) {
if (owner != null && owner.contains(".")) {
throw new IllegalArgumentException("Attempt to instance a MemberInfo with an invalid owner format");
}
this.owner = owner;
this.name = name;
this.desc = desc;
this.matchAll = matchAll;
this.forceField = false;
this.unparsed = unparsed;
}
/**
* Initialise a MemberInfo using the supplied insn which must be an instance
* of MethodInsnNode or FieldInsnNode.
*
* @param insn instruction node to copy values from
*/
public MemberInfo(AbstractInsnNode insn) {
this.matchAll = false;
this.forceField = false;
this.unparsed = null;
if (insn instanceof MethodInsnNode) {
MethodInsnNode methodNode = (MethodInsnNode) insn;
this.owner = methodNode.owner;
this.name = methodNode.name;
this.desc = methodNode.desc;
} else if (insn instanceof FieldInsnNode) {
FieldInsnNode fieldNode = (FieldInsnNode) insn;
this.owner = fieldNode.owner;
this.name = fieldNode.name;
this.desc = fieldNode.desc;
} else {
throw new IllegalArgumentException("insn must be an instance of MethodInsnNode or FieldInsnNode");
}
}
/**
* Initialise a MemberInfo using the supplied mapping object
*
* @param mapping Mapping object to copy values from
*/
public MemberInfo(IMapping<?> mapping) {
this.owner = mapping.getOwner();
this.name = mapping.getSimpleName();
this.desc = mapping.getDesc();
this.matchAll = false;
this.forceField = mapping.getType() == IMapping.Type.FIELD;
this.unparsed = null;
}
/**
* Initialise a remapped MemberInfo using the supplied mapping object
*
* @param method mapping method object to copy values from
*/
private MemberInfo(MemberInfo remapped, MappingMethod method, boolean setOwner) {
this.owner = setOwner ? method.getOwner() : remapped.owner;
this.name = method.getSimpleName();
this.desc = method.getDesc();
this.matchAll = remapped.matchAll;
this.forceField = false;
this.unparsed = null;
}
/**
* Initialise a remapped MemberInfo with a new name
*
* @param original Original MemberInfo
* @param owner new owner
*/
private MemberInfo(MemberInfo original, String owner) {
this.owner = owner;
this.name = original.name;
this.desc = original.desc;
this.matchAll = original.matchAll;
this.forceField = original.forceField;
this.unparsed = null;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
String owner = this.owner != null ? "L" + this.owner + ";" : "";
String name = this.name != null ? this.name : "";
String qualifier = this.matchAll ? "*" : "";
String desc = this.desc != null ? this.desc : "";
String separator = desc.startsWith("(") ? "" : (this.desc != null ? ":" : "");
return owner + name + qualifier + separator + desc;
}
/**
* Return this MemberInfo as an SRG mapping
*
* @return SRG representation of this MemberInfo
* @deprecated use m.asMethodMapping().serialise() instead
*/
@Deprecated
public String toSrg() {
if (!this.isFullyQualified()) {
throw new MixinException("Cannot convert unqualified reference to SRG mapping");
}
if (this.desc.startsWith("(")) {
return this.owner + "/" + this.name + " " + this.desc;
}
return this.owner + "/" + this.name;
}
/**
* Returns this MemberInfo as a java-style descriptor
*/
public String toDescriptor() {
if (this.desc == null) {
return "";
}
return new SignaturePrinter(this).setFullyQualified(true).toDescriptor();
}
/**
* Returns the <em>constructor type</em> represented by this MemberInfo
*/
public String toCtorType() {
if (this.unparsed == null) {
return null;
}
String returnType = this.getReturnType();
if (returnType != null) {
return returnType;
}
if (this.owner != null) {
return this.owner;
}
if (this.name != null && this.desc == null) {
return this.name;
}
return this.desc != null ? this.desc : this.unparsed;
}
/**
* Returns the <em>constructor descriptor</em> represented by this
* MemberInfo, returns null if no descriptor is present.
*/
public String toCtorDesc() {
if (this.desc != null && this.desc.startsWith("(") && this.desc.indexOf(')') > -1) {
return this.desc.substring(0, this.desc.indexOf(')') + 1) + "V";
}
return null;
}
/**
* Get the return type for this MemberInfo, if the decriptor is present,
* returns null if the descriptor is absent or if this MemberInfo represents
* a field
*/
public String getReturnType() {
if (this.desc == null || this.desc.indexOf(')') == -1 || this.desc.indexOf('(') != 0 ) {
return null;
}
String returnType = this.desc.substring(this.desc.indexOf(')') + 1);
if (returnType.startsWith("L") && returnType.endsWith(";")) {
return returnType.substring(1, returnType.length() - 1);
}
return returnType;
}
/**
* Returns this MemberInfo as a {@link MappingField} or
* {@link MappingMethod}
*/
public IMapping<?> asMapping() {
return this.isField() ? this.asFieldMapping() : this.asMethodMapping();
}
/**
* Returns this MemberInfo as a mapping method
*/
public MappingMethod asMethodMapping() {
if (!this.isFullyQualified()) {
throw new MixinException("Cannot convert unqualified reference " + this + " to MethodMapping");
}
if (this.isField()) {
throw new MixinException("Cannot convert a non-method reference " + this + " to MethodMapping");
}
return new MappingMethod(this.owner, this.name, this.desc);
}
/**
* Returns this MemberInfo as a mapping field
*/
public MappingField asFieldMapping() {
if (!this.isField()) {
throw new MixinException("Cannot convert non-field reference " + this + " to FieldMapping");
}
return new MappingField(this.owner, this.name, this.desc);
}
/**
* Get whether this reference is fully qualified
*
* @return true if all components of this reference are non-null
*/
public boolean isFullyQualified() {
return this.owner != null && this.name != null && this.desc != null;
}
/**
* Get whether this MemberInfo is definitely a field, the output of this
* method is undefined if {@link #isFullyQualified} returns false.
*
* @return true if this is definitely a field
*/
public boolean isField() {
return this.forceField || (this.desc != null && !this.desc.startsWith("("));
}
/**
* Perform ultra-simple validation of the descriptor, checks that the parts
* of the descriptor are basically sane.
*
* @return fluent
*
* @throws InvalidMemberDescriptorException if any validation check fails
*/
public MemberInfo validate() throws InvalidMemberDescriptorException {
// Extremely naive class name validation, just to spot really egregious errors
if (this.owner != null) {
if (!this.owner.matches("(?i)^[\\w\\p{Sc}/]+$")) {
throw new InvalidMemberDescriptorException("Invalid owner: " + this.owner);
}
try {
if (!this.owner.equals(Type.getType(this.owner).getDescriptor())) {
throw new InvalidMemberDescriptorException("Invalid owner type specified: " + this.owner);
}
} catch (Exception ex) {
throw new InvalidMemberDescriptorException("Invalid owner type specified: " + this.owner);
}
}
// Also naive validation, we're looking for stupid errors here
if (this.name != null && !this.name.matches("(?i)^<?[\\w\\p{Sc}]+>?$")) {
throw new InvalidMemberDescriptorException("Invalid name: " + this.name);
}
if (this.desc != null) {
if (!this.desc.matches("^(\\([\\w\\p{Sc}\\[/;]*\\))?\\[?[\\w\\p{Sc}/;]+$")) {
throw new InvalidMemberDescriptorException("Invalid descriptor: " + this.desc);
}
if (this.isField()) {
if (!this.desc.equals(Type.getType(this.desc).getDescriptor())) {
throw new InvalidMemberDescriptorException("Invalid field type in descriptor: " + this.desc);
}
} else {
try {
Type.getArgumentTypes(this.desc);
} catch (Exception ex) {
throw new InvalidMemberDescriptorException("Invalid descriptor: " + this.desc);
}
String retString = this.desc.substring(this.desc.indexOf(')') + 1);
try {
Type retType = Type.getType(retString);
if (!retString.equals(retType.getDescriptor())) {
throw new InvalidMemberDescriptorException("Invalid return type \"" + retString + "\" in descriptor: " + this.desc);
}
} catch (Exception ex) {
throw new InvalidMemberDescriptorException("Invalid return type \"" + retString + "\" in descriptor: " + this.desc);
}
}
}
return this;
}
/**
* Test whether this MemberInfo matches the supplied values. Null values are
* ignored.
* @param owner Owner to compare with, null to skip
* @param name Name to compare with, null to skip
* @param desc Signature to compare with, null to skip
* @return true if all non-null values in this reference match non-null
* arguments supplied to this method
*/
public boolean matches(String owner, String name, String desc) {
return this.matches(owner, name, desc, 0);
}
/**
* Test whether this MemberInfo matches the supplied values at the specified
* ordinal. Null values are ignored.
*
* @param owner Owner to compare with, null to skip
* @param name Name to compare with, null to skip
* @param desc Signature to compare with, null to skip
* @param ordinal ordinal position within the class, used to honour the
* matchAll semantics
* @return true if all non-null values in this reference match non-null
* arguments supplied to this method
*/
public boolean matches(String owner, String name, String desc, int ordinal) {
if (this.desc != null && desc != null && !this.desc.equals(desc)) {
return false;
}
if (this.name != null && name != null && !this.name.equals(name)) {
return false;
}
if (this.owner != null && owner != null && !this.owner.equals(owner)) {
return false;
}
return ordinal == 0 || this.matchAll;
}
/**
* Test whether this MemberInfo matches the supplied values. Null values are
* ignored.
* @param name Name to compare with, null to skip
* @param desc Signature to compare with, null to skip
* @return true if all non-null values in this reference match non-null
* arguments supplied to this method
*/
public boolean matches(String name, String desc) {
return this.matches(name, desc, 0);
}
/**
* Test whether this MemberInfo matches the supplied values at the specified
* ordinal. Null values are ignored.
*
* @param name Name to compare with, null to skip
* @param desc Signature to compare with, null to skip
* @param ordinal ordinal position within the class, used to honour the
* matchAll semantics
* @return true if all non-null values in this reference match non-null
* arguments supplied to this method
*/
public boolean matches(String name, String desc, int ordinal) {
return (this.name == null || this.name.equals(name))
&& (this.desc == null || (desc != null && desc.equals(this.desc)))
&& (ordinal == 0 || this.matchAll);
}
/**
* Create a new version of this member with a different owner
*
* @param newOwner New owner for this member
*/
public MemberInfo move(String newOwner) {
if ((newOwner == null && this.owner == null) || (newOwner != null && newOwner.equals(this.owner))) {
return this;
}
return new MemberInfo(this, newOwner);
}
/**
* Create a remapped version of this member using the supplied method data
*
* @param srgMethod SRG method data to use
* @param setOwner True to set the owner as well as the name
* @return New MethodInfo with remapped values
*/
public MemberInfo remapUsing(MappingMethod srgMethod, boolean setOwner) {
return new MemberInfo(this, srgMethod, setOwner);
}
/**
* Parse a MemberInfo from a string and perform validation
*
* @param string String to parse MemberInfo from
* @return parsed MemberInfo
*/
public static MemberInfo parseAndValidate(String string) throws InvalidMemberDescriptorException {
return MemberInfo.parse(string, null, null).validate();
}
/**
* Parse a MemberInfo from a string and perform validation
*
* @param string String to parse MemberInfo from
* @param context Context to use for reference mapping
* @return parsed MemberInfo
*/
public static MemberInfo parseAndValidate(String string, IMixinContext context) throws InvalidMemberDescriptorException {
return MemberInfo.parse(string, context.getReferenceMapper(), context.getClassRef()).validate();
}
/**
* Parse a MemberInfo from a string
*
* @param string String to parse MemberInfo from
* @return parsed MemberInfo
*/
public static MemberInfo parse(String string) {
return MemberInfo.parse(string, null, null);
}
/**
* Parse a MemberInfo from a string
*
* @param string String to parse MemberInfo from
* @param context Context to use for reference mapping
* @return parsed MemberInfo
*/
public static MemberInfo parse(String string, IMixinContext context) {
return MemberInfo.parse(string, context.getReferenceMapper(), context.getClassRef());
}
/**
* Parse a MemberInfo from a string
*
* @param input String to parse MemberInfo from
* @param refMapper Reference mapper to use
* @param mixinClass Mixin class to use for remapping
* @return parsed MemberInfo
*/
private static MemberInfo parse(String input, ReferenceMapper refMapper, String mixinClass) {
String desc = null;
String owner = null;
String name = Strings.nullToEmpty(input).replaceAll("\\s", "");
if (refMapper != null) {
name = refMapper.remap(mixinClass, name);
}
int lastDotPos = name.lastIndexOf('.');
int semiColonPos = name.indexOf(';');
if (lastDotPos > -1) {
owner = name.substring(0, lastDotPos).replace('.', '/');
name = name.substring(lastDotPos + 1);
} else if (semiColonPos > -1 && name.startsWith("L")) {
owner = name.substring(1, semiColonPos).replace('.', '/');
name = name.substring(semiColonPos + 1);
}
int parenPos = name.indexOf('(');
int colonPos = name.indexOf(':');
if (parenPos > -1) {
desc = name.substring(parenPos);
name = name.substring(0, parenPos);
} else if (colonPos > -1) {
desc = name.substring(colonPos + 1);
name = name.substring(0, colonPos);
}
if ((name.indexOf('/') > -1 || name.indexOf('.') > -1) && owner == null) {
owner = name;
name = "";
}
boolean matchAll = name.endsWith("*");
if (matchAll) {
name = name.substring(0, name.length() - 1);
}
if (name.isEmpty()) {
name = null;
}
return new MemberInfo(name, owner, desc, matchAll, input);
}
/**
* Return the supplied mapping parsed as a MemberInfo
*
* @param mapping mapping to parse
* @return new MemberInfo
*/
public static MemberInfo fromMapping(IMapping<?> mapping) {
return new MemberInfo(mapping);
}
}