Add support for access modifier mapping

Allows for easily changing arbitrary symbol access flags. Supports
FML *_at.cfg access transformer file format loading in AccessMap.
Multiple AT's can be merged together and applied simultaneously
from the remapper 'preprocessor' class. Symbol visibility, final,
as well as any Java access flag can be set or cleared with +/-.
This commit is contained in:
Agaricus 2013-03-07 19:57:29 -08:00
parent 6ba34dbfee
commit 3ae83a6b81
4 changed files with 364 additions and 25 deletions

View File

@ -0,0 +1,136 @@
/**
* Copyright (c) 2012-2013, md_5. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* The name of the author may not be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.md_5.specialsource;
import lombok.ToString;
import lombok.libs.org.objectweb.asm.Opcodes;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Represents symbol access specifiers to be added or removed
*/
@ToString
public class AccessChange {
private int clear; // bits to clear to 0
private int set; // bits to set to 1 (overrides clear)
private final static Map<String, Integer> accessCodes = new HashMap<String, Integer>();
static {
accessCodes.put("public", Opcodes.ACC_PUBLIC);
accessCodes.put("private", Opcodes.ACC_PRIVATE);
accessCodes.put("protected", Opcodes.ACC_PROTECTED);
accessCodes.put("default", 0);
accessCodes.put("", 0);
accessCodes.put("package-private", 0);
accessCodes.put("static", Opcodes.ACC_STATIC);
accessCodes.put("final", Opcodes.ACC_FINAL);
accessCodes.put("f", Opcodes.ACC_FINAL); // FML
accessCodes.put("super", Opcodes.ACC_SUPER);
accessCodes.put("synchronized", Opcodes.ACC_SYNCHRONIZED);
accessCodes.put("volatile", Opcodes.ACC_VOLATILE);
accessCodes.put("bridge", Opcodes.ACC_BRIDGE);
accessCodes.put("varargs", Opcodes.ACC_VARARGS);
accessCodes.put("transient", Opcodes.ACC_TRANSIENT);
accessCodes.put("native", Opcodes.ACC_NATIVE);
accessCodes.put("interface", Opcodes.ACC_INTERFACE);
accessCodes.put("abstract", Opcodes.ACC_ABSTRACT);
accessCodes.put("strict", Opcodes.ACC_STRICT);
accessCodes.put("synthetic", Opcodes.ACC_SYNTHETIC);
accessCodes.put("annotation", Opcodes.ACC_ANNOTATION);
accessCodes.put("enum", Opcodes.ACC_ENUM);
accessCodes.put("deprecated", Opcodes.ACC_DEPRECATED);
}
private final static int MASK_ALL_VISIBILITY = Opcodes.ACC_PUBLIC | Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED;
public AccessChange(String s) {
String[] parts = s.split("(?=[+-])"); // preserve delimiters
if (parts.length < 1) {
throw new IllegalArgumentException("Invalid access string: " + s);
}
// Symbol visibility
clear = MASK_ALL_VISIBILITY; // always clear lower 3 bits, so visibility can be set below
String visibilityString = parts[0];
set = accessCodes.get(visibilityString);
if (set > Opcodes.ACC_PROTECTED) {
throw new IllegalArgumentException("Invalid access visibility: " + visibilityString);
}
// Modifiers
for (int i = 1; i < parts.length; ++i) {
if (parts[i].length() < 2) {
throw new IllegalArgumentException("Invalid modifier length "+parts[i]+" in access string: " + s);
}
// Name
char actionChar = parts[i].charAt(0);
String modifierString = parts[i].substring(1);
int modifier;
if (!accessCodes.containsKey(modifierString)) {
throw new IllegalArgumentException("Invalid modifier string "+modifierString+" in access string: " + s);
}
modifier = accessCodes.get(modifierString);
// Toggle
switch (actionChar) {
case '+': set |= modifier; break;
case '-': clear |= modifier; break;
default: throw new IllegalArgumentException("Invalid action "+actionChar+" in access string: " + s);
}
}
}
public int apply(int access) {
access &= ~clear;
access |= set;
return access;
}
/**
* Combine this access change with another, setting/clearing bits from both
*/
public void merge(AccessChange rhs) {
clear |= rhs.clear;
if ((rhs.set & MASK_ALL_VISIBILITY) != 0) {
// visibility change - clear old visibility bits
set &= ~MASK_ALL_VISIBILITY;
}
set |= rhs.set;
}
}

View File

@ -0,0 +1,156 @@
/**
* Copyright (c) 2012-2013, md_5. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* The name of the author may not be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.md_5.specialsource;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class AccessMap {
private Map<String, AccessChange> map = new HashMap<String, AccessChange>();
public AccessMap() {
}
public void loadAccessTransformer(BufferedReader reader) throws IOException {
String line;
while ((line = reader.readLine()) != null) {
// strip comments and trailing whitespace
int n = line.indexOf('#');
if (n != -1) {
line = line.substring(0, n);
}
n = line.lastIndexOf(' ');
if (n != -1) {
line = line.substring(0, n);
}
if (line.isEmpty()){
continue;
}
// _at.cfg format:
// protected/public/private[+/-modifiers] symbol
n = line.indexOf(' ');
if (n == -1) {
throw new IOException("loadAccessTransformer invalid line: " + line);
}
String accessString = line.substring(0, n);
String symbolString = line.substring(n + 1);
addAccessChange(symbolString, accessString);
}
}
public void loadAccessTransformer(File file) throws IOException {
loadAccessTransformer(new BufferedReader(new FileReader(file)));
}
public void loadAccessTransformer(String filename) throws IOException {
loadAccessTransformer(URLDownloader.getLocalFile(filename));
}
/**
* Convert a symbol name pattern from AT config to internal format
*/
public static String convertSymbolPattern(String s) {
// source name to internal name
s = s.replace('.', '/');
// method descriptor separated from name by a space
if (s.indexOf('(') != -1) {
s = s.replaceFirst("(?=[^ ])[(]", " (");
}
// now it matches the symbol name format used in the rest of SpecialSource
// (but also possibly with wildcards)
return s;
}
public void addAccessChange(String symbolString, String accessString) {
addAccessChange(convertSymbolPattern(symbolString), new AccessChange(accessString));
}
public void addAccessChange(String key, AccessChange accessChange) {
if (map.containsKey(key)) {
System.out.println("INFO: merging AccessMap "+key+" from "+map.get(key)+" with "+accessChange);
map.get(key).merge(accessChange);
}
map.put(key, accessChange);
}
public int applyClassAccess(String className, int access) {
int old = access;
access = apply(className, access);
access = apply("*", access);
//System.out.println("AT: class: "+className+" "+old+" -> "+access); // TODO: debug logging
return access;
}
public int applyFieldAccess(String className, String fieldName, int access) {
int old = access;
access = apply("*/*", access);
access = apply(className + "/*", access);
access = apply(className + "/" + fieldName, access);
//System.out.println("AT: field: "+className+"/"+fieldName+" "+old+" -> "+access);
return access;
}
public int applyMethodAccess(String className, String methodName, String methodDesc, int access) {
int old = access;
access = apply("*/* ()", access);
access = apply(className + "/* ()", access);
access = apply(className + "/" + methodName + " " + methodDesc, access);
//System.out.println("AT: method: "+className+"/"+methodName+" "+methodDesc+" "+old+" -> "+access);
return access;
}
private int apply(String key, int existing) {
AccessChange change = map.get(key);
if (change == null) {
return existing;
} else {
return change.apply(existing);
}
}
}

View File

@ -46,9 +46,15 @@ public class JarRemapper extends Remapper {
private static final int CLASS_LEN = ".class".length();
public final JarMapping jarMapping;
public RemapperPreprocessor remapperPreprocessor;
public JarRemapper(RemapperPreprocessor remapperPreprocessor, JarMapping jarMapping) {
this.remapperPreprocessor = remapperPreprocessor;
this.jarMapping = jarMapping;
}
public JarRemapper(JarMapping jarMapping) {
this.jarMapping = jarMapping;
this(null, jarMapping);
}
@Override
@ -152,18 +158,25 @@ public class JarRemapper extends Remapper {
* Remap an individual class given an InputStream to its bytecode
*/
public byte[] remapClassFile(InputStream is) throws IOException {
ClassReader reader = new ClassReader(is);
ClassWriter wr = new ClassWriter(0);
RemappingClassAdapter mapper = new RemappingClassAdapter(wr, this);
reader.accept(mapper, ClassReader.EXPAND_FRAMES); // TODO: EXPAND_FRAMES necessary?
return wr.toByteArray();
return remapClassFile(new ClassReader(is));
}
public byte[] remapClassFile(byte[] in) {
ClassReader reader = new ClassReader(in);
return remapClassFile(new ClassReader(in));
}
private byte[] remapClassFile(ClassReader reader) {
if (remapperPreprocessor != null) {
byte[] pre = remapperPreprocessor.preprocess(reader);
if (pre != null) {
reader = new ClassReader(pre);
}
}
ClassWriter wr = new ClassWriter(0);
RemappingClassAdapter mapper = new RemappingClassAdapter(wr, this);
reader.accept(mapper, ClassReader.EXPAND_FRAMES); // TODO: EXPAND_FRAMES necessary?
return wr.toByteArray();
}
}

View File

@ -52,6 +52,7 @@ public class RemapperPreprocessor {
private InheritanceMap inheritanceMap;
private JarMapping jarMapping;
private AccessMap accessMap;
/**
*
@ -59,26 +60,39 @@ public class RemapperPreprocessor {
* @param jarMapping Mapping for reflection remapping, or null to not remap reflection
* @throws IOException
*/
@SuppressWarnings("unchecked")
public RemapperPreprocessor(InheritanceMap inheritanceMap, JarMapping jarMapping) {
public RemapperPreprocessor(InheritanceMap inheritanceMap, JarMapping jarMapping, AccessMap accessMap) {
this.inheritanceMap = inheritanceMap;
this.jarMapping = jarMapping;
this.accessMap = accessMap;
}
public RemapperPreprocessor(InheritanceMap inheritanceMap, JarMapping jarMapping) {
this(inheritanceMap, jarMapping, null);
}
public byte[] preprocess(InputStream inputStream) throws IOException {
return preprocess(new ClassReader(inputStream));
}
public byte[] preprocess(byte[] bytecode) throws IOException {
return preprocess(new ClassReader(bytecode));
}
@SuppressWarnings("unchecked")
public byte[] preprocess(String className, InputStream inputStream) throws IOException {
ClassReader classReader = new ClassReader(inputStream);
public byte[] preprocess(ClassReader classReader) {
byte[] bytecode = null;
ClassNode classNode = new ClassNode();
int flags = ClassReader.SKIP_DEBUG;
byte[] bytecode = null;
if (jarMapping == null) {
// No reflection remapping - skip the code
if (!isRewritingNeeded()) {
// Not rewriting the class - skip the code, not needed
flags |= ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES;
}
classReader.accept(classNode, flags);
String className = classNode.name;
// Inheritance extraction
if (inheritanceMap != null) {
logI("Loading plugin class inheritance for "+className);
@ -96,11 +110,25 @@ public class RemapperPreprocessor {
logI("Inheritance added "+className+" parents "+parents.size());
}
// Reflection remapping
if (jarMapping != null) {
ClassWriter cw = new ClassWriter(0);
if (isRewritingNeeded()) {
// Class access
if (accessMap != null) {
classNode.access = accessMap.applyClassAccess(className, classNode.access);
}
// Field access
if (accessMap != null) {
for (FieldNode fieldNode : (List<FieldNode>) classNode.fields) {
fieldNode.access = accessMap.applyFieldAccess(className, fieldNode.name, fieldNode.access);
}
}
for (MethodNode methodNode : (List<MethodNode>) classNode.methods) {
// Method access
methodNode.access = accessMap.applyMethodAccess(className, methodNode.name, methodNode.desc, methodNode.access);
// Reflection remapping
if (jarMapping != null) {
AbstractInsnNode insn = methodNode.instructions.getFirst();
while (insn != null) {
if (insn.getOpcode() == Opcodes.INVOKEVIRTUAL) {
@ -110,7 +138,9 @@ public class RemapperPreprocessor {
insn = insn.getNext();
}
}
}
ClassWriter cw = new ClassWriter(0);
classNode.accept(cw);
bytecode = cw.toByteArray();
}
@ -118,6 +148,10 @@ public class RemapperPreprocessor {
return bytecode;
}
private boolean isRewritingNeeded() {
return jarMapping != null || accessMap != null;
}
/**
* Replace class.getDeclaredField("string") with a remapped field string
* @param insn Method call instruction