From 3ae83a6b8169ecd7ba8d53723ca23930cd562649 Mon Sep 17 00:00:00 2001 From: Agaricus Date: Thu, 7 Mar 2013 19:57:29 -0800 Subject: [PATCH] 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 +/-. --- .../net/md_5/specialsource/AccessChange.java | 136 +++++++++++++++ .../net/md_5/specialsource/AccessMap.java | 156 ++++++++++++++++++ .../net/md_5/specialsource/JarRemapper.java | 27 ++- .../specialsource/RemapperPreprocessor.java | 70 ++++++-- 4 files changed, 364 insertions(+), 25 deletions(-) create mode 100644 src/main/java/net/md_5/specialsource/AccessChange.java create mode 100644 src/main/java/net/md_5/specialsource/AccessMap.java diff --git a/src/main/java/net/md_5/specialsource/AccessChange.java b/src/main/java/net/md_5/specialsource/AccessChange.java new file mode 100644 index 0000000..2fbe803 --- /dev/null +++ b/src/main/java/net/md_5/specialsource/AccessChange.java @@ -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 accessCodes = new HashMap(); + + 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; + } +} diff --git a/src/main/java/net/md_5/specialsource/AccessMap.java b/src/main/java/net/md_5/specialsource/AccessMap.java new file mode 100644 index 0000000..34bb680 --- /dev/null +++ b/src/main/java/net/md_5/specialsource/AccessMap.java @@ -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 map = new HashMap(); + + 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); + } + } +} diff --git a/src/main/java/net/md_5/specialsource/JarRemapper.java b/src/main/java/net/md_5/specialsource/JarRemapper.java index dea2ab3..f70ab30 100644 --- a/src/main/java/net/md_5/specialsource/JarRemapper.java +++ b/src/main/java/net/md_5/specialsource/JarRemapper.java @@ -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(); } } diff --git a/src/main/java/net/md_5/specialsource/RemapperPreprocessor.java b/src/main/java/net/md_5/specialsource/RemapperPreprocessor.java index af5a81a..babc9ee 100644 --- a/src/main/java/net/md_5/specialsource/RemapperPreprocessor.java +++ b/src/main/java/net/md_5/specialsource/RemapperPreprocessor.java @@ -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,21 +110,37 @@ 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); + } - for (MethodNode methodNode : (List) classNode.methods) { - AbstractInsnNode insn = methodNode.instructions.getFirst(); - while (insn != null) { - if (insn.getOpcode() == Opcodes.INVOKEVIRTUAL) { - remapGetDeclaredField(insn); - } - - insn = insn.getNext(); + // Field access + if (accessMap != null) { + for (FieldNode fieldNode : (List) classNode.fields) { + fieldNode.access = accessMap.applyFieldAccess(className, fieldNode.name, fieldNode.access); } } + for (MethodNode methodNode : (List) 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) { + remapGetDeclaredField(insn); + } + + 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