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