├── core ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── javax.annotation.processing.Processor │ │ └── java │ │ └── io │ │ └── tunabytes │ │ ├── bytecode │ │ ├── ReadClassException.java │ │ ├── introspect │ │ │ ├── MixinInfo.java │ │ │ ├── MixinField.java │ │ │ ├── MixinMethod.java │ │ │ ├── MixinFieldVisitor.java │ │ │ ├── MixinClassVisitor.java │ │ │ └── MixinMethodVisitor.java │ │ ├── editor │ │ │ ├── DefinalizeEditor.java │ │ │ ├── OverwriteEditor.java │ │ │ ├── MixinsEditor.java │ │ │ ├── MethodMergerEditor.java │ │ │ ├── InjectionEditor.java │ │ │ └── AccessorEditor.java │ │ ├── MixinsConfig.java │ │ ├── MixinEntry.java │ │ └── MixinsBootstrap.java │ │ ├── Definalize.java │ │ ├── classloader │ │ ├── ClassDefiner.java │ │ ├── TunaClassDefiner.java │ │ └── SecurityActions.java │ │ ├── ActualType.java │ │ ├── Overwrite.java │ │ ├── Mixin.java │ │ ├── Mirror.java │ │ ├── Accessor.java │ │ ├── Inject.java │ │ └── ap │ │ └── MixinsProcessor.java └── pom.xml ├── jitpack.yml ├── .gitignore ├── pom.xml ├── java9 ├── pom.xml └── src │ └── main │ └── java │ └── io │ └── tunabytes │ └── classloader │ └── Java9.java ├── java11 ├── pom.xml └── src │ └── main │ └── java │ └── io │ └── tunabytes │ └── classloader │ └── Java11.java ├── java8 ├── pom.xml └── src │ └── main │ └── java │ └── io │ └── tunabytes │ └── classloader │ └── Java8.java ├── README.md └── LICENSE /core/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | io.tunabytes.ap.MixinsProcessor 2 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sdk install java 9.0.4-oracle 3 | - sdk use java 9.0.4-oracle 4 | - sdk install java 11.0.10-open 5 | - sdk use java 11.0.10-open 6 | jdk: 7 | - openjdk8 8 | - oraclejdk9 9 | - openjdk11 -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/ReadClassException.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode; 2 | 3 | public class ReadClassException extends RuntimeException { 4 | 5 | public ReadClassException(String message) { 6 | super(message); 7 | } 8 | 9 | public ReadClassException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java template 2 | # Compiled class file 3 | *.class 4 | 5 | # Log file 6 | *.log 7 | 8 | # BlueJ files 9 | *.ctxt 10 | 11 | # Mobile Tools for Java (J2ME) 12 | .mtj.tmp/ 13 | 14 | # Package Files # 15 | *.jar 16 | *.war 17 | *.nar 18 | *.ear 19 | *.zip 20 | *.tar.gz 21 | *.rar 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | 26 | 27 | # Project exclude paths 28 | /target/ -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/introspect/MixinInfo.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.introspect; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | import java.util.List; 8 | 9 | @ToString 10 | @Getter 11 | @AllArgsConstructor 12 | public class MixinInfo { 13 | 14 | private final String mixinName, mixinInternalName; 15 | private final boolean mixinInterface; 16 | private final List fields; 17 | private final List methods; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/Definalize.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * An annotation added to {@link Mirror}s to mark their targetted fields or methods 10 | * as non-final. 11 | *

12 | * See {@link Mirror} for more information 13 | */ 14 | @Target({ElementType.FIELD, ElementType.METHOD}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface Definalize { 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/introspect/MixinField.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.introspect; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | import org.objectweb.asm.tree.FieldNode; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | @ToString 11 | public final class MixinField { 12 | 13 | private final int access; 14 | private final boolean mirror; 15 | private final boolean definalize; 16 | private final String name, desc; 17 | private final boolean remapped; 18 | private final String type; 19 | private final FieldNode node; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/classloader/ClassDefiner.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.classloader; 2 | 3 | import java.security.ProtectionDomain; 4 | 5 | /** 6 | * A simple interface for providing cross-compatibility across Java versions to 7 | * define classes from their bytecode. 8 | */ 9 | public interface ClassDefiner { 10 | 11 | Class defineClass(String name, 12 | byte[] b, 13 | int off, 14 | int len, 15 | Class neighbor, 16 | ClassLoader loader, 17 | ProtectionDomain protectionDomain) throws ClassFormatError; 18 | 19 | default boolean requiresNeighbor() { 20 | return false; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.tunabytes 8 | tuna-bytes 9 | pom 10 | 1.2.0 11 | 12 | 13 | 1.8 14 | 1.8 15 | 16 | 17 | 18 | core 19 | java8 20 | java9 21 | java11 22 | 23 | -------------------------------------------------------------------------------- /java9/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | tuna-bytes 7 | io.tunabytes 8 | 1.2.0 9 | 10 | 11 | 4.0.0 12 | 13 | 14 | 9 15 | 9 16 | 17 | 18 | java9 19 | 20 | 21 | 22 | io.tunabytes 23 | core 24 | 1.2.0 25 | compile 26 | 27 | 28 | -------------------------------------------------------------------------------- /java11/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | tuna-bytes 7 | io.tunabytes 8 | 1.2.0 9 | 10 | 11 | 4.0.0 12 | 13 | 14 | 11 15 | 11 16 | 17 | 18 | java11 19 | 20 | 21 | 22 | io.tunabytes 23 | core 24 | 1.2.0 25 | compile 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /java8/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | tuna-bytes 7 | io.tunabytes 8 | 1.2.0 9 | 10 | 11 | 4.0.0 12 | 13 | 14 | 1.8 15 | 1.8 16 | 17 | 18 | java8 19 | 20 | 21 | 22 | io.tunabytes 23 | core 24 | 1.2.0 25 | compile 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/ActualType.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * An annotation that allows to explicitly specify the type of a field, method or parameter. 10 | *

11 | * Since the JVM requires matching the exact field or method signature in the bytecode, 12 | * it may be impossible to get certain methods or fields whose types or signatures 13 | * are of inaccessible classes. This annotation allows to override this and 14 | * get the type remapped accordingly. 15 | */ 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) 18 | public @interface ActualType { 19 | 20 | /** 21 | * The class name of which this type is composed of 22 | * 23 | * @return The full binary class name. 24 | */ 25 | String value(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/Overwrite.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * An annotation to mark the mixin method as completely overwrite the 10 | * targeted method's code. 11 | *

12 | * The mixins method name and signature must exactly match the targeted method. 13 | * That is: 14 | *

19 | *

20 | * Note that having multiple {@link Overwrite}s on the same method would simply lead 21 | * to one overwrite being dismissed. Hence, it is not recommended to have more 22 | * than one {@link Overwrite} for each method. 23 | */ 24 | @Target(ElementType.METHOD) 25 | @Retention(RetentionPolicy.RUNTIME) 26 | public @interface Overwrite { 27 | 28 | /** 29 | * The name of the method being overwritten. 30 | * 31 | * @return The method name 32 | */ 33 | String value() default ""; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/introspect/MixinMethod.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.introspect; 2 | 3 | import io.tunabytes.Inject.At; 4 | import org.objectweb.asm.Opcodes; 5 | import org.objectweb.asm.Type; 6 | import org.objectweb.asm.tree.MethodNode; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | 11 | @ToString 12 | @Getter 13 | @AllArgsConstructor 14 | public final class MixinMethod { 15 | 16 | private final String name; 17 | private final int access; 18 | private final Type descriptor; 19 | private final String realDescriptor; 20 | private final int injectLine; 21 | private final String injectMethod; 22 | private final At injectAt; 23 | private final boolean overwrite, accessor, inject, mirror, definalize, requireTypeRemapping; 24 | private final String mirrorName; 25 | private final String overwrittenName; // or accessed method 26 | private final String accessedProperty; // or accessed method 27 | private final MethodNode methodNode; 28 | private final CallType type; 29 | 30 | public enum CallType { 31 | INVOKE, 32 | GET, 33 | SET 34 | } 35 | 36 | public boolean isPrivate() { 37 | return (access & Opcodes.ACC_PRIVATE) != 0; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/editor/DefinalizeEditor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.editor; 2 | 3 | import io.tunabytes.bytecode.introspect.MixinMethod; 4 | import org.objectweb.asm.Opcodes; 5 | import org.objectweb.asm.tree.ClassNode; 6 | import org.objectweb.asm.tree.FieldNode; 7 | import io.tunabytes.bytecode.introspect.MixinField; 8 | import io.tunabytes.bytecode.introspect.MixinInfo; 9 | import lombok.SneakyThrows; 10 | import org.objectweb.asm.tree.MethodNode; 11 | 12 | /** 13 | * A mixins editor for processing {@link io.tunabytes.Definalize} fields. 14 | */ 15 | public class DefinalizeEditor implements MixinsEditor { 16 | 17 | @SneakyThrows @Override public void edit(ClassNode node, MixinInfo info) { 18 | for (MixinField field : info.getFields()) { 19 | if (field.isDefinalize() && field.isMirror()) { 20 | FieldNode fnode = node.fields.stream().filter(c -> c.name.equals(field.getName())).findFirst() 21 | .orElseThrow(() -> new NoSuchFieldException(field.getName())); 22 | fnode.access &= ~Opcodes.ACC_FINAL; 23 | } 24 | } 25 | for (MixinMethod method : info.getMethods()) { 26 | if (method.isDefinalize() && method.isMirror()) { 27 | MethodNode mnode = node.methods.stream().filter(c -> c.name.equals(method.getName())).findFirst() 28 | .orElseThrow(() -> new NoSuchFieldException(method.getName())); 29 | mnode.access &= ~Opcodes.ACC_FINAL; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/Mixin.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * The main entrypoint for a tuna mixin class. This annotation marks that 10 | * the annotated member will be manipulating the specified class. 11 | *

12 | * This annotation will be scanned by {@link io.tunabytes.ap.MixinsProcessor the annotation processor}, 13 | * which is required for tuna mixins to work. 14 | *

15 | * This annotation can be added to classes and interfaces: 16 | *

24 | */ 25 | @Target(ElementType.TYPE) 26 | @Retention(RetentionPolicy.CLASS) 27 | public @interface Mixin { 28 | 29 | /** 30 | * The class we're targeting. 31 | * 32 | * @return The class we will be mixing into 33 | */ 34 | Class value() default Object.class; 35 | 36 | /** 37 | * The name of the class, an alternative way in case of inaccessible classes 38 | * 39 | * @return The name of the class 40 | */ 41 | String name() default ""; 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/Mirror.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Represents an annotation to mark a field or a method inside a {@link Mixin} class as a mirror: 10 | * 25 | */ 26 | @Target({ElementType.FIELD, ElementType.METHOD}) 27 | @Retention(RetentionPolicy.RUNTIME) 28 | public @interface Mirror { 29 | 30 | /** 31 | * The underlying field or method that is being mirrored. 32 | * 33 | * @return The target field or method name 34 | */ 35 | String value() default ""; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /java8/src/main/java/io/tunabytes/classloader/Java8.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.classloader; 2 | 3 | import java.lang.reflect.Method; 4 | import java.security.ProtectionDomain; 5 | 6 | final class Java8 implements ClassDefiner { 7 | 8 | private final Method defineClass = getDefineClassMethod(); 9 | private final SecurityActions stack = SecurityActions.stack; 10 | 11 | private Method getDefineClassMethod() { 12 | try { 13 | return SecurityActions.getDeclaredMethod(ClassLoader.class, "defineClass", 14 | new Class[]{ 15 | String.class, byte[].class, int.class, int.class, ProtectionDomain.class 16 | }); 17 | } catch (NoSuchMethodException e) { 18 | throw new RuntimeException("cannot initialize", e); 19 | } 20 | } 21 | 22 | @Override 23 | public Class defineClass(String name, byte[] b, int off, int len, Class neighbor, 24 | ClassLoader loader, ProtectionDomain protectionDomain) 25 | throws ClassFormatError { 26 | try { 27 | SecurityActions.setAccessible(defineClass, true); 28 | return (Class) defineClass.invoke(loader, new Object[]{ 29 | name, b, off, len, protectionDomain 30 | }); 31 | } catch (Throwable e) { 32 | sneakyThrow(e); 33 | return null; 34 | } 35 | } 36 | 37 | private static RuntimeException sneakyThrow(Throwable t) { 38 | if (t == null) throw new NullPointerException("t"); 39 | return sneakyThrow0(t); 40 | } 41 | 42 | private static T sneakyThrow0(Throwable t) throws T { 43 | throw (T) t; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/editor/OverwriteEditor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.editor; 2 | 3 | import io.tunabytes.bytecode.introspect.MixinField; 4 | import io.tunabytes.bytecode.introspect.MixinInfo; 5 | import io.tunabytes.bytecode.introspect.MixinMethod; 6 | import lombok.SneakyThrows; 7 | import org.objectweb.asm.tree.*; 8 | 9 | /** 10 | * A mixins editor for processing {@link io.tunabytes.Overwrite} methods. 11 | */ 12 | public class OverwriteEditor implements MixinsEditor { 13 | 14 | @SneakyThrows @Override public void edit(ClassNode classNode, MixinInfo info) { 15 | for (MixinField field : info.getFields()) { 16 | if (field.isMirror()) continue; 17 | classNode.fields.add(field.getNode()); 18 | } 19 | for (MixinMethod method : info.getMethods()) { 20 | if (method.isInject()) continue; 21 | if (method.isMirror()) continue; 22 | if (method.isOverwrite()) { 23 | MethodNode node = method.getMethodNode(); 24 | if ((node.access & ACC_ABSTRACT) != 0) { 25 | throw new IllegalArgumentException("@Overwrite cannot be used on abstract methods! (" + node.name + " in " + info.getMixinName() + ")"); 26 | } 27 | MethodNode underlying = classNode.methods.stream().filter(c -> c.name.equals(method.getOverwrittenName()) && c.desc.equals(node.desc)) 28 | .findFirst().orElseThrow(() -> new NoSuchMethodException(method.getOverwrittenName())); 29 | underlying.instructions = new InsnList(); 30 | underlying.instructions.add(node.instructions); 31 | for (AbstractInsnNode instruction : underlying.instructions) { 32 | remapInstruction(classNode, info, instruction); 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/introspect/MixinFieldVisitor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.introspect; 2 | 3 | import io.tunabytes.ActualType; 4 | import io.tunabytes.Definalize; 5 | import io.tunabytes.Mirror; 6 | import org.objectweb.asm.*; 7 | import org.objectweb.asm.tree.FieldNode; 8 | 9 | public class MixinFieldVisitor extends FieldVisitor { 10 | 11 | private static final Type MIRROR = Type.getType(Mirror.class); 12 | private static final Type DEFINALIZE = Type.getType(Definalize.class); 13 | private static final String ACTUAL_TYPE = Type.getDescriptor(ActualType.class); 14 | 15 | protected boolean mirror, definalize, remapped; 16 | protected Type type; 17 | protected String name, desc; 18 | 19 | public MixinFieldVisitor(FieldNode node) { 20 | super(Opcodes.ASM8, node); 21 | desc = node.desc; 22 | } 23 | 24 | @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 25 | boolean mirrorAnn = MIRROR.getDescriptor().equals(descriptor); 26 | if (mirrorAnn) 27 | mirror = true; 28 | if (DEFINALIZE.getDescriptor().equals(descriptor)) 29 | definalize = true; 30 | return new AnnotationVisitor(Opcodes.ASM8) { 31 | @Override public void visit(String name, Object value) { 32 | if (ACTUAL_TYPE.equals(descriptor)) { 33 | remapped = true; 34 | desc = MixinMethodVisitor.fromActualType(desc, (String) value).getDescriptor(); 35 | } 36 | if (mirrorAnn && name.equals("value")) { 37 | MixinFieldVisitor.this.name = (String) value; 38 | } 39 | } 40 | }; 41 | } 42 | 43 | @Override public void visitAttribute(Attribute attribute) { 44 | super.visitAttribute(attribute); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/MixinsConfig.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode; 2 | 3 | import io.tunabytes.classloader.TunaClassDefiner; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.*; 8 | 9 | import static java.util.Objects.requireNonNull; 10 | 11 | final class MixinsConfig { 12 | 13 | private final List mixinEntries = new ArrayList<>(); 14 | private final Map> neighbors = new LinkedHashMap<>(); 15 | 16 | public MixinsConfig() { 17 | try { 18 | InputStream configStream = getClass().getResourceAsStream("/mixins.properties"); 19 | requireNonNull(configStream, "mixins.properties not found. Did you add tuna-bytes as an annotation processor?"); 20 | Properties properties = new Properties(); 21 | properties.load(configStream); 22 | properties.forEach((key, value) -> mixinEntries.add(new MixinEntry((String) key, (String) value))); 23 | InputStream neighborsStream = getClass().getResourceAsStream("/mixins-neighbors.properties"); 24 | requireNonNull(neighborsStream, "mixins-neighbors.properties not found. Did you add tuna-bytes as an annotation processor?"); 25 | if (TunaClassDefiner.requiresNeighbor()) { 26 | Properties neighborsProps = new Properties(); 27 | neighborsProps.load(neighborsStream); 28 | neighborsProps.forEach((key, value) -> { 29 | try { 30 | neighbors.put((String) key, Class.forName(String.valueOf(value))); 31 | } catch (ClassNotFoundException e) { 32 | e.printStackTrace(); 33 | } 34 | }); 35 | } 36 | } catch (IOException e) { 37 | e.printStackTrace(); 38 | } 39 | } 40 | 41 | public Class getNeighbor(String name) { 42 | return neighbors.get(name.substring(0, name.lastIndexOf('.'))); 43 | } 44 | 45 | public List getMixinEntries() { 46 | return mixinEntries; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/MixinEntry.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode; 2 | 3 | import org.objectweb.asm.ClassReader; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.Set; 8 | 9 | final class MixinEntry { 10 | 11 | private final String mixinClass; 12 | private final String targetClass; 13 | private ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 14 | 15 | public MixinEntry(String mixinClass, String targetClass) { 16 | this.mixinClass = mixinClass; 17 | this.targetClass = targetClass; 18 | } 19 | 20 | public String getMixinClass() { 21 | return mixinClass; 22 | } 23 | 24 | public String getTargetClass() { 25 | return targetClass; 26 | } 27 | 28 | public ClassReader mixinReader() { 29 | try (InputStream stream = getClass().getClassLoader().getResourceAsStream(mixinClass.replace('.', '/') + ".class")) { 30 | if (stream != null) { 31 | return new ClassReader(stream); 32 | } 33 | throw new IllegalStateException("Class not found: " + mixinClass + ". Make sure you specify any additional classloaders in MixinsBootstrap.init(...)!"); 34 | } catch (IOException e) { 35 | throw new ReadClassException(mixinClass, e); 36 | } 37 | } 38 | 39 | public ClassReader targetReader(Set classLoaders) { 40 | for (ClassLoader loader : classLoaders) { 41 | try (InputStream stream = loader.getResourceAsStream(targetClass.replace('.', '/') + ".class")) { 42 | if (stream != null) { 43 | classLoader = loader; 44 | return new ClassReader(stream); 45 | } 46 | } catch (IOException e) { 47 | throw new ReadClassException(targetClass, e); 48 | } 49 | } 50 | throw new IllegalStateException("Class not found: " + targetClass + ". Make sure you specify any additional classloaders in MixinsBootstrap.init(...)!"); 51 | } 52 | 53 | public ClassLoader getClassLoader() { 54 | return classLoader; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/ReflxctionDev/Tuna-Bytes.svg)](https://jitpack.io/#ReflxctionDev/Tuna-Bytes) 2 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 3 | 4 | # Tuna Bytes 5 | ![A tuna byte :)](https://i.imgur.com/15VLkMI.jpg) 6 | Tuna-bytes is an all-purpose powerful class and bytecode manipulation mixins for Java, which is intended at those with minimal understanding of the Java bytecode structure. 7 | 8 | ## Features 9 | - Full support for the notorious Java 9+ versions, as well as Java 8. 10 | - Does not require access to the source code of classes, and works well even on third-party resources. 11 | - Does not require any knowledge of the Java bytecode. 12 | - Requires zero overhead to get started. Just add Tuna-Bytes as a dependency and as an annotation processor, and Tuna-Bytes will handle the rest. 13 | - Does not require any additional Java execution arguments, like what Java agents do. 14 | 15 | ## Index 16 | Check the [wiki](https://github.com/ReflxctionDev/Tuna-Bytes/wiki) for a full overview on the library. 17 | 1. [Maven setup](https://github.com/ReflxctionDev/Tuna-Bytes/wiki/Maven-Setup) 18 | 2. [Gradle setup](https://github.com/ReflxctionDev/Tuna-Bytes/wiki/Gradle-Setup) 19 | 3. [Getting started](https://github.com/ReflxctionDev/Tuna-Bytes/wiki/Getting-started) 20 | 4. [**Example**: Overwrite a method](https://github.com/ReflxctionDev/Tuna-Bytes/wiki/Overwrite) 21 | 5. [**Example**: Inject into a method](https://github.com/ReflxctionDev/Tuna-Bytes/wiki/Injecting) 22 | 6. [**Example**: Create accessors for inaccessible fields and methods](https://github.com/ReflxctionDev/Tuna-Bytes/wiki/Accessors) 23 | 7. [**Example**: Mirroring a field or a method](https://github.com/ReflxctionDev/Tuna-Bytes/wiki/Mirroring) 24 | 25 | # Drawbacks 26 | Just like any other bytecode manipulation library, **manipulating a class after is has been loaded is not possible** without things like instrumentation, agents or such. Tuna-bytes assumes that any class it is about to modify has not been loaded, and will otherwise throw an exception. To suppress `Class XX has already been loaded` exceptions, use `MixinsBootstrap.init(true)` 27 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/Accessor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Marks a method or property accessor. 10 | *

11 | * This can be used to access inaccessible fields or methods, in which it will 12 | * infer the method's functionality and target from the annotated method name. 13 | *

14 | * Example: 15 | *

16 |  * public class Point {
17 |  *
18 |  *     private final int x;
19 |  *     private final int y;
20 |  *
21 |  *     public Point(int x, int y) {
22 |  *         this.x = x;
23 |  *         this.y = y;
24 |  *     }
25 |  *
26 |  *     private void secretMethod() {
27 |  *         System.out.println("top secret");
28 |  *     }
29 |  *
30 |  * }
31 |  *
32 |  * @Mixin(Point.class)
33 |  * public interface PointAccessor {
34 |  *
35 |  *     @Accessor int getX();
36 |  *
37 |  *     @Accessor int getY();
38 |  *
39 |  *     @Accessor void callSecretMethod();
40 |  *
41 |  * }
42 |  * 
43 | * We can then access the properties by simply casting the Point object 44 | * to a PointAccessor: 45 | *
46 |  *     Point point = new Point(10, -41);
47 |  *     ((PointAccessor) point).getX();
48 |  *     ((PointAccessor) point).getY();
49 |  *     ((PointAccessor) point).callSecretMethod();
50 |  * 
51 | *

52 | * Getter and setter accessor method names should follow the common naming conventions 53 | * to get the property name evaluated correctly. For invoking methods, they should be prefixed 54 | * with invoke or call: 55 | *

    56 | *
  • getAbc() will infer the property 'abc'
  • 57 | *
  • setURL() will infer the property 'uRL'!
  • 58 | *
  • callMethod() will infer the method method()
  • 59 | *
  • invokeSomething() will infer the method something()
  • 60 | *
61 | *

62 | * Alternatively, you can specify the property or method name inside the {@link Accessor#value()} 63 | * when inferring may go wrong. 64 | */ 65 | @Target(ElementType.METHOD) 66 | @Retention(RetentionPolicy.RUNTIME) 67 | public @interface Accessor { 68 | 69 | /** 70 | * The name of the method or property being accessed. 71 | * 72 | * @return The property or method name. 73 | */ 74 | String value() default ""; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | tuna-bytes 7 | io.tunabytes 8 | 1.2.0 9 | 10 | 4.0.0 11 | 12 | core 13 | 14 | 15 | 1.8 16 | 1.8 17 | 18 | 19 | 20 | 21 | org.ow2.asm 22 | asm 23 | 9.2 24 | 25 | 26 | org.ow2.asm 27 | asm-tree 28 | 9.2 29 | 30 | 31 | org.projectlombok 32 | lombok 33 | 1.18.20 34 | provided 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-compiler-plugin 43 | 3.8.1 44 | 45 | 46 | 47 | org.projectlombok 48 | lombok 49 | 1.18.20 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-source-plugin 57 | 3.2.0 58 | 59 | 60 | attach-sources 61 | 62 | jar 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/classloader/TunaClassDefiner.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.classloader; 2 | 3 | import java.security.ProtectionDomain; 4 | import java.util.Collection; 5 | import java.util.List; 6 | 7 | public final class TunaClassDefiner { 8 | 9 | /** 10 | * The major version number of class files 11 | * for JDK 1.8. 12 | */ 13 | public static final int JAVA_8 = 52; 14 | 15 | /** 16 | * The major version number of class files 17 | * for JDK 1.9. 18 | */ 19 | public static final int JAVA_9 = 53; 20 | 21 | /** 22 | * The major version number of class files 23 | * for JDK 10. 24 | */ 25 | public static final int JAVA_10 = 54; 26 | 27 | /** 28 | * The major version number of class files 29 | * for JDK 11. 30 | */ 31 | public static final int JAVA_11 = 55; 32 | 33 | /** 34 | * The least supported version respecting the 35 | * current one 36 | */ 37 | public static final int MAJOR_VERSION; 38 | 39 | static { 40 | int ver = JAVA_8; 41 | try { 42 | Class.forName("java.lang.Module"); 43 | ver = JAVA_9; 44 | List.class.getMethod("copyOf", Collection.class); 45 | ver = JAVA_10; 46 | Class.forName("java.util.Optional").getMethod("isEmpty"); 47 | ver = JAVA_11; 48 | } catch (Throwable ignored) {} 49 | MAJOR_VERSION = ver; 50 | } 51 | 52 | // Java 11+ removed sun.misc.Unsafe.defineClass, so we fallback to invoking defineClass on 53 | // ClassLoader until we have an implementation that uses MethodHandles.Lookup.defineClass 54 | private static final ClassDefiner classDefiner = MAJOR_VERSION > JAVA_10 55 | ? createDefiner("11") 56 | : MAJOR_VERSION >= JAVA_9 ? createDefiner("9") 57 | : createDefiner("8"); 58 | 59 | private static ClassDefiner createDefiner(String name) { 60 | try { 61 | return Class.forName("io.tunabytes.classloader.Java" + name).asSubclass(ClassDefiner.class).newInstance(); 62 | } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { 63 | throw new IllegalArgumentException("Unable to create ClassDefiner for Java " + name, e); 64 | } 65 | } 66 | 67 | public static Class defineClass(String name, 68 | byte[] b, 69 | Class neighbor, 70 | ClassLoader loader, 71 | ProtectionDomain protectionDomain) throws ClassFormatError { 72 | return classDefiner.defineClass(name, b, 0, b.length, neighbor, loader, protectionDomain); 73 | } 74 | 75 | public static boolean requiresNeighbor() { 76 | return classDefiner.requiresNeighbor(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/editor/MixinsEditor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.editor; 2 | 3 | import io.tunabytes.bytecode.introspect.MixinField; 4 | import io.tunabytes.bytecode.introspect.MixinInfo; 5 | import io.tunabytes.bytecode.introspect.MixinMethod; 6 | import org.objectweb.asm.Opcodes; 7 | import org.objectweb.asm.tree.AbstractInsnNode; 8 | import org.objectweb.asm.tree.ClassNode; 9 | import org.objectweb.asm.tree.FieldInsnNode; 10 | import org.objectweb.asm.tree.MethodInsnNode; 11 | 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | /** 16 | * Represents a transformer for editing class nodes. 17 | */ 18 | public interface MixinsEditor extends Opcodes { 19 | 20 | List RETURN_OPCODES = Arrays.asList( 21 | RETURN, 22 | ARETURN, 23 | IRETURN, 24 | DRETURN, 25 | FRETURN, 26 | LRETURN 27 | ); 28 | 29 | /** 30 | * Edits the class node as needed. 31 | * 32 | * @param node Class node to edit. 33 | * @param info Information about the mixin being transformed 34 | */ 35 | void edit(ClassNode node, MixinInfo info); 36 | 37 | /** 38 | * Applies simple changes to methods and fields instructions to make sure they 39 | * have correct references 40 | * 41 | * @param classNode Class node to remap to 42 | * @param info Mixins information 43 | * @param instruction The instruction to remap 44 | */ 45 | default void remapInstruction(ClassNode classNode, MixinInfo info, AbstractInsnNode instruction) { 46 | if (instruction instanceof FieldInsnNode) { 47 | FieldInsnNode insn = (FieldInsnNode) instruction; 48 | if (insn.owner.equals(info.getMixinInternalName())) { 49 | insn.owner = classNode.name; 50 | info.getFields().stream() 51 | .filter(MixinField::isRemapped) 52 | .filter(c -> c.getType().equals(insn.desc)) 53 | .findFirst() 54 | .ifPresent(field -> insn.desc = field.getDesc()); 55 | } 56 | } 57 | if (instruction instanceof MethodInsnNode) { 58 | MethodInsnNode insn = (MethodInsnNode) instruction; 59 | if (insn.getOpcode() == INVOKEINTERFACE && insn.itf && insn.owner.equals(info.getMixinInternalName())) { 60 | insn.setOpcode(INVOKEVIRTUAL); 61 | insn.itf = false; 62 | } 63 | if (insn.owner.equals(info.getMixinInternalName())) { 64 | insn.owner = classNode.name; 65 | info.getMethods().stream() 66 | .filter(MixinMethod::isRequireTypeRemapping) 67 | .filter(c -> c.getRealDescriptor().equals(insn.desc)) 68 | .findFirst() 69 | .ifPresent(method -> insn.desc = method.getDescriptor().getDescriptor()); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/editor/MethodMergerEditor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.editor; 2 | 3 | import io.tunabytes.bytecode.introspect.MixinInfo; 4 | import io.tunabytes.bytecode.introspect.MixinMethod; 5 | import org.objectweb.asm.Opcodes; 6 | import org.objectweb.asm.tree.*; 7 | 8 | /** 9 | * A mixins editor for copying methods from the mixins class to the target class. 10 | */ 11 | public class MethodMergerEditor implements MixinsEditor { 12 | 13 | @Override public void edit(ClassNode classNode, MixinInfo info) { 14 | for (MixinMethod method : info.getMethods()) { 15 | if (method.isOverwrite()) continue; 16 | if (method.isAccessor()) continue; 17 | if (method.isInject()) continue; 18 | if (method.isMirror()) continue; 19 | // inject no-args constructors into each constructor of the mixed class 20 | if (method.getName().equals("")) { 21 | if (method.getDescriptor().getArgumentTypes().length == 0) { 22 | InsnList list = method.getMethodNode().instructions; 23 | for (AbstractInsnNode node : list) { 24 | if (node instanceof LineNumberNode || RETURN_OPCODES.contains(node.getOpcode())) 25 | list.remove(node); 26 | else if (node.getOpcode() == Opcodes.INVOKESPECIAL) { 27 | MethodInsnNode nm = (MethodInsnNode) node; 28 | if (nm.name.equals("") && nm.owner.equals("java/lang/Object")) 29 | list.remove(nm); 30 | } else 31 | remapInstruction(classNode, info, node); 32 | } 33 | for (MethodNode c : classNode.methods) { 34 | if (c.name.equals("")) { 35 | AbstractInsnNode lastReturn = null; 36 | for (AbstractInsnNode n : c.instructions) { 37 | if (RETURN_OPCODES.contains(n.getOpcode())) lastReturn = n; 38 | } 39 | c.instructions.insertBefore(lastReturn, InjectionEditor.cloneInsnList(list)); 40 | } 41 | } 42 | } 43 | continue; 44 | } 45 | if (info.isMixinInterface() && (method.getMethodNode().access & ACC_ABSTRACT) != 0) continue; 46 | MethodNode mn = method.getMethodNode(); 47 | MethodNode underlying = new MethodNode(mn.access, mn.name, mn.desc, mn.signature, mn.exceptions.toArray(new String[0])); 48 | underlying.instructions = new InsnList(); 49 | underlying.instructions.add(mn.instructions); 50 | for (AbstractInsnNode instruction : underlying.instructions) { 51 | remapInstruction(classNode, info, instruction); 52 | } 53 | classNode.methods.add(underlying); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /java11/src/main/java/io/tunabytes/classloader/Java11.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.classloader; 2 | 3 | import java.lang.invoke.MethodHandles; 4 | import java.lang.invoke.MethodHandles.Lookup; 5 | import java.lang.reflect.Method; 6 | import java.security.ProtectionDomain; 7 | 8 | final class Java11 implements ClassDefiner { 9 | 10 | private final Method defineClass = getDefineClassMethod(); 11 | 12 | private Method getDefineClassMethod() { 13 | try { 14 | return SecurityActions.getDeclaredMethod(ClassLoader.class, "defineClass", 15 | new Class[]{ 16 | String.class, byte[].class, int.class, int.class, ProtectionDomain.class 17 | }); 18 | } catch (NoSuchMethodException e) { 19 | throw new RuntimeException("cannot initialize", e); 20 | } 21 | } 22 | 23 | @Override 24 | public Class defineClass(String name, byte[] b, int off, int len, Class neighbor, 25 | ClassLoader loader, ProtectionDomain protectionDomain) 26 | throws ClassFormatError { 27 | if (neighbor != null) 28 | return toClass(neighbor, b); 29 | else { 30 | // Lookup#defineClass() is not available. So fallback to invoking defineClass on 31 | // ClassLoader, which causes a warning message. 32 | 33 | try { 34 | SecurityActions.setAccessible(defineClass, true); 35 | return (Class) defineClass.invoke(loader, new Object[]{ 36 | name, b, off, len, protectionDomain 37 | }); 38 | } catch (Throwable e) { 39 | sneakyThrow(e); 40 | return null; 41 | } 42 | } 43 | } 44 | 45 | private static RuntimeException sneakyThrow(Throwable t) { 46 | if (t == null) throw new NullPointerException("t"); 47 | return sneakyThrow0(t); 48 | } 49 | 50 | private static T sneakyThrow0(Throwable t) throws T { 51 | throw (T) t; 52 | } 53 | 54 | /** 55 | * Loads a class file by {@code java.lang.invoke.MethodHandles.Lookup}. 56 | * It is obtained by using {@code neighbor}. 57 | * 58 | * @param neighbor a class belonging to the same package that the loaded 59 | * class belogns to. 60 | * @param bcode the bytecode. 61 | * @since 3.24 62 | */ 63 | public static Class toClass(Class neighbor, byte[] bcode) { 64 | try { 65 | TunaClassDefiner.class.getModule().addReads(neighbor.getModule()); 66 | Lookup lookup = MethodHandles.lookup(); 67 | Lookup prvlookup = MethodHandles.privateLookupIn(neighbor, lookup); 68 | return prvlookup.defineClass(bcode); 69 | } catch (IllegalAccessException | IllegalArgumentException e) { 70 | throw new IllegalArgumentException(e.getMessage() + ": " + neighbor.getName() 71 | + " has no permission to define the class"); 72 | } 73 | } 74 | 75 | @Override public boolean requiresNeighbor() { 76 | return true; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/introspect/MixinClassVisitor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.introspect; 2 | 3 | import org.objectweb.asm.*; 4 | import org.objectweb.asm.tree.FieldNode; 5 | import org.objectweb.asm.tree.MethodNode; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class MixinClassVisitor extends ClassVisitor { 11 | 12 | private final List fields = new ArrayList<>(); 13 | private final List methods = new ArrayList<>(); 14 | private boolean isInterface; 15 | private String name; 16 | private MixinInfo info; 17 | 18 | public MixinClassVisitor() { 19 | super(Opcodes.ASM8); 20 | } 21 | 22 | @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 23 | if ((access & Opcodes.ACC_INTERFACE) != 0) 24 | isInterface = true; 25 | this.name = name.replace('/', '.'); 26 | super.visit(version, access, name, signature, superName, interfaces); 27 | } 28 | 29 | @Override public FieldVisitor visitField(int access, String fname, String descriptor, String signature, Object value) { 30 | return new MixinFieldVisitor(new FieldNode(access, fname, descriptor, signature, value)) { 31 | @Override public void visitEnd() { 32 | FieldNode node = (FieldNode) fv; 33 | node.desc = desc; 34 | fields.add(new MixinField(access, mirror, definalize, name == null ? fname : name, desc, remapped, descriptor, (FieldNode) fv)); 35 | } 36 | }; 37 | } 38 | 39 | @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 40 | return new MixinMethodVisitor(new MethodNode(access, name, descriptor, signature, exceptions)) { 41 | @Override public void visitEnd() { 42 | Type desc = Type.getMethodType(returnType, argumentTypes); 43 | node.desc = desc.getDescriptor(); 44 | methods.add(new MixinMethod( 45 | name, 46 | access, 47 | desc, 48 | descriptor, 49 | injectLine, 50 | injectMethodName, 51 | injectAt, 52 | overwrite, 53 | accessor, 54 | inject, 55 | mirror, 56 | definalize, 57 | remap, 58 | mirrorName == null ? name : mirrorName, 59 | overwrittenName == null ? name : overwrittenName, 60 | accessorName == null ? getActualName(name) : accessorName, 61 | node, 62 | type 63 | )); 64 | } 65 | }; 66 | } 67 | 68 | @Override public void visitEnd() { 69 | info = new MixinInfo(name, name.replace('.', '/'), isInterface, fields, methods); 70 | super.visitEnd(); 71 | } 72 | 73 | public MixinInfo getInfo() { 74 | return info; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/Inject.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Represents a method snap that is injected at a specific position in 12 | * another method. 13 | *

14 | * Note: Injected code will be directly translated into the targeted method, and not wrapped 15 | * into an external method invocation. For example, any {@code return} in the injected code 16 | * will translate into a {@code return} in the target method. 17 | */ 18 | @Target(ElementType.METHOD) 19 | @Retention(RetentionPolicy.RUNTIME) 20 | public @interface Inject { 21 | 22 | /** 23 | * The method name that is being targeted 24 | * 25 | * @return The target method name to inject into. 26 | */ 27 | String method(); 28 | 29 | /** 30 | * Where the injection will occur 31 | * 32 | * @return Injection position. 33 | * @see At 34 | */ 35 | At at(); 36 | 37 | /** 38 | * The line number for injection. This must be used in tandem 39 | * with {@link At#BEFORE_LINE} or {@link At#AFTER_LINE}. 40 | *

41 | * Note: The line number must match the number in the source code. Since 42 | * line numbers are preserved into the bytecode as instructions when the class is 43 | * compiled, they will be used as a reference point. Binary lines will not work and 44 | * most likely going to yield incorrect lines. 45 | * 46 | * @return The line number to inject before of after. 47 | */ 48 | int lineNumber() default 0; 49 | 50 | /** 51 | * Represents an injection point 52 | */ 53 | enum At { 54 | 55 | /** 56 | * Injects at the very beginning of the method 57 | */ 58 | BEGINNING, 59 | 60 | /** 61 | * Injects at the very end of the method. This will be invoked just before 62 | * the last return statement. 63 | */ 64 | END, 65 | 66 | /** 67 | * Injects before each {@code return} in the method. This should be used in methods 68 | * that have complex returning flow, in which it is desired to call the injection 69 | * after the method has finished executing. 70 | *

71 | * Note that, even if no return is actually in the source in void methods, 72 | * there is an invisible return at the end of the void method. 73 | */ 74 | BEFORE_EACH_RETURN, 75 | 76 | /** 77 | * Injects before the given line in {@link Inject#lineNumber()}. 78 | * 79 | * @see Inject#lineNumber() 80 | */ 81 | BEFORE_LINE, 82 | 83 | /** 84 | * Injects after the given line in {@link Inject#lineNumber()}. 85 | * 86 | * @see Inject#lineNumber() 87 | */ 88 | AFTER_LINE; 89 | 90 | private static final Map BY_NAME = new HashMap<>(); 91 | 92 | public static At at(String at) { return BY_NAME.get(at); } 93 | 94 | static { for (At at : values()) BY_NAME.put(at.name(), at); } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /java9/src/main/java/io/tunabytes/classloader/Java9.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.classloader; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.reflect.Method; 6 | import java.security.ProtectionDomain; 7 | import java.util.List; 8 | 9 | final class Java9 implements ClassDefiner { 10 | 11 | static final class ReferencedUnsafe { 12 | 13 | private final SecurityActions.TheUnsafe sunMiscUnsafeTheUnsafe; 14 | private final MethodHandle defineClass; 15 | 16 | ReferencedUnsafe(SecurityActions.TheUnsafe usf, MethodHandle meth) { 17 | sunMiscUnsafeTheUnsafe = usf; 18 | defineClass = meth; 19 | } 20 | 21 | Class defineClass(String name, byte[] b, int off, int len, 22 | ClassLoader loader, ProtectionDomain protectionDomain) 23 | throws ClassFormatError { 24 | try { 25 | return (Class) defineClass.invokeWithArguments( 26 | sunMiscUnsafeTheUnsafe.theUnsafe, 27 | name, b, off, len, loader, protectionDomain); 28 | } catch (Throwable e) { 29 | sneakyThrow(e); 30 | return null; 31 | } 32 | } 33 | } 34 | 35 | 36 | private static RuntimeException sneakyThrow(Throwable t) { 37 | if (t == null) throw new NullPointerException("t"); 38 | return sneakyThrow0(t); 39 | } 40 | 41 | private static T sneakyThrow0(Throwable t) throws T { 42 | throw (T) t; 43 | } 44 | 45 | private final ReferencedUnsafe sunMiscUnsafe = getReferencedUnsafe(); 46 | 47 | public Java9() { 48 | Class stackWalkerClass = null; 49 | try { 50 | stackWalkerClass = Class.forName("java.lang.StackWalker"); 51 | } catch (ClassNotFoundException e) { 52 | // Skip initialization when the class doesn't exist i.e. we are on JDK < 9 53 | } 54 | if (stackWalkerClass != null) { 55 | try { 56 | Class optionClass = Class.forName("java.lang.StackWalker$Option"); 57 | } catch (Throwable e) { 58 | throw new RuntimeException("cannot initialize", e); 59 | } 60 | } 61 | } 62 | 63 | private ReferencedUnsafe getReferencedUnsafe() { 64 | try { 65 | SecurityActions.TheUnsafe usf = SecurityActions.getSunMiscUnsafeAnonymously(); 66 | List defineClassMethod = usf.methods.get("defineClass"); 67 | // On Java 11+ the defineClass method does not exist anymore 68 | if (null == defineClassMethod) 69 | return null; 70 | MethodHandle meth = MethodHandles.lookup().unreflect(defineClassMethod.get(0)); 71 | return new ReferencedUnsafe(usf, meth); 72 | } catch (Throwable e) { 73 | throw new RuntimeException("cannot initialize", e); 74 | } 75 | } 76 | 77 | @Override 78 | public Class defineClass(String name, byte[] b, int off, int len, Class neighbor, 79 | ClassLoader loader, ProtectionDomain protectionDomain) 80 | throws ClassFormatError { 81 | return sunMiscUnsafe.defineClass(name, b, off, len, loader, 82 | protectionDomain); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/editor/InjectionEditor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.editor; 2 | 3 | import io.tunabytes.Inject.At; 4 | import io.tunabytes.bytecode.introspect.MixinInfo; 5 | import io.tunabytes.bytecode.introspect.MixinMethod; 6 | import lombok.SneakyThrows; 7 | import org.objectweb.asm.tree.*; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * A mixins editor for processing {@link io.tunabytes.Inject} methods. 14 | */ 15 | public class InjectionEditor implements MixinsEditor { 16 | 17 | @SneakyThrows @Override public void edit(ClassNode classNode, MixinInfo info) { 18 | for (MixinMethod method : info.getMethods()) { 19 | if (!method.isInject()) continue; 20 | if ((method.getMethodNode().access & ACC_ABSTRACT) != 0) { 21 | throw new IllegalArgumentException("@Inject cannot be used on abstract methods! (" + method.getMethodNode().name + " in " + info.getMixinName() + ")"); 22 | } 23 | At at = method.getInjectAt(); 24 | int line = method.getInjectLine(); 25 | String injectIn = method.getInjectMethod(); 26 | MethodNode targetMethod = classNode.methods.stream().filter(c -> c.name.equals(injectIn)) 27 | .findFirst().orElseThrow(() -> new NoSuchMethodException(injectIn)); 28 | InsnList list = method.getMethodNode().instructions; 29 | for (AbstractInsnNode instruction : list) { 30 | remapInstruction(classNode, info, instruction); 31 | } 32 | 33 | AbstractInsnNode lastInjectedReturn = null; 34 | for (AbstractInsnNode abstractInsnNode : list) { 35 | if (abstractInsnNode instanceof LineNumberNode) { 36 | list.remove(abstractInsnNode); 37 | } else if (RETURN_OPCODES.contains(abstractInsnNode.getOpcode())) { 38 | lastInjectedReturn = abstractInsnNode; 39 | } 40 | } 41 | 42 | if (lastInjectedReturn != null) list.remove(lastInjectedReturn); 43 | 44 | if (at == At.BEGINNING) { 45 | AbstractInsnNode first = targetMethod.instructions.getFirst(); 46 | if (first != null) { 47 | targetMethod.instructions.insert(first, list); 48 | } else 49 | targetMethod.instructions.add(list); 50 | } else if (at == At.END) { 51 | AbstractInsnNode lastReturn = null; 52 | for (AbstractInsnNode instruction : targetMethod.instructions) { 53 | if (instruction instanceof InsnNode && RETURN_OPCODES.contains(instruction.getOpcode())) 54 | lastReturn = instruction; 55 | } 56 | targetMethod.instructions.insertBefore(lastReturn, list); 57 | } else if (at == At.BEFORE_EACH_RETURN) { 58 | for (AbstractInsnNode insnNode : targetMethod.instructions) { 59 | if (RETURN_OPCODES.contains(insnNode.getOpcode())) { 60 | targetMethod.instructions.insertBefore(insnNode, cloneInsnList(list)); 61 | } 62 | } 63 | } else if (at == At.BEFORE_LINE) { 64 | for (AbstractInsnNode insnNode : targetMethod.instructions) { 65 | if (!(insnNode instanceof LineNumberNode)) continue; 66 | int currentLine = ((LineNumberNode) insnNode).line; 67 | if (currentLine == line) 68 | targetMethod.instructions.insertBefore(insnNode, list); 69 | } 70 | } else if (at == At.AFTER_LINE) { 71 | for (AbstractInsnNode insnNode : targetMethod.instructions) { 72 | if (!(insnNode instanceof LineNumberNode)) continue; 73 | int currentLine = ((LineNumberNode) insnNode).line; 74 | if (currentLine == line) 75 | targetMethod.instructions.insert(insnNode, list); 76 | } 77 | } 78 | } 79 | } 80 | 81 | private static Map cloneLabels(InsnList insns) { 82 | Map labelMap = new HashMap<>(); 83 | for (AbstractInsnNode insn = insns.getFirst(); insn != null; insn = insn.getNext()) { 84 | if (insn.getType() == 8) { 85 | labelMap.put((LabelNode) insn, new LabelNode()); 86 | } 87 | } 88 | return labelMap; 89 | } 90 | 91 | public static InsnList cloneInsnList(InsnList insns) { 92 | return cloneInsnList(cloneLabels(insns), insns); 93 | } 94 | 95 | private static InsnList cloneInsnList(Map labelMap, InsnList insns) { 96 | InsnList clone = new InsnList(); 97 | for (AbstractInsnNode insn = insns.getFirst(); insn != null; insn = insn.getNext()) { 98 | clone.add(insn.clone(labelMap)); 99 | } 100 | return clone; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/MixinsBootstrap.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode; 2 | 3 | import io.tunabytes.bytecode.editor.*; 4 | import io.tunabytes.bytecode.introspect.MixinClassVisitor; 5 | import io.tunabytes.bytecode.introspect.MixinInfo; 6 | import io.tunabytes.classloader.TunaClassDefiner; 7 | import lombok.AllArgsConstructor; 8 | import lombok.SneakyThrows; 9 | import org.objectweb.asm.ClassReader; 10 | import org.objectweb.asm.ClassWriter; 11 | import org.objectweb.asm.tree.ClassNode; 12 | 13 | import java.util.*; 14 | import java.util.Map.Entry; 15 | 16 | /** 17 | * A class for applying changes from mixins to actual classes. 18 | * 19 | * @see #init(boolean) 20 | * @see #init() 21 | */ 22 | public final class MixinsBootstrap { 23 | 24 | private MixinsBootstrap() { } 25 | 26 | /** 27 | * Initializes and applies mixins, and throws an exception on each loaded class. 28 | */ 29 | public static void init() { 30 | init(false); 31 | } 32 | 33 | /** 34 | * Initializes and applies mixins 35 | * 36 | * @param ignoreLoadedClasses Whether should we ignore any class that has been alreade loaded. 37 | * If false, an {@link IllegalStateException} will be thrown if 38 | * a class appears to be loaded. 39 | */ 40 | @SneakyThrows public static void init(boolean ignoreLoadedClasses) { 41 | init(ignoreLoadedClasses, Collections.emptyList()); 42 | } 43 | 44 | /** 45 | * Initializes and applies mixins 46 | * 47 | * @param ignoreLoadedClasses Whether should we ignore any class that has been alreade loaded. 48 | * If false, an {@link IllegalStateException} will be thrown if 49 | * a class appears to be loaded. 50 | * @param searchClassLoaders A list of additional classloaders to search classes for. 51 | */ 52 | public static void init(boolean ignoreLoadedClasses, Collection searchClassLoaders) { 53 | Set classLoaders = new LinkedHashSet<>(); 54 | classLoaders.add(Thread.currentThread().getContextClassLoader()); 55 | classLoaders.addAll(searchClassLoaders); 56 | List editors = new ArrayList<>(); 57 | editors.add(new DefinalizeEditor()); 58 | editors.add(new OverwriteEditor()); 59 | editors.add(new AccessorEditor()); 60 | editors.add(new InjectionEditor()); 61 | editors.add(new MethodMergerEditor()); 62 | MixinsConfig config = new MixinsConfig(); 63 | Map writers = new HashMap<>(); 64 | for (MixinEntry mixinEntry : config.getMixinEntries()) { 65 | ClassReader reader = mixinEntry.mixinReader(); 66 | MixinClassVisitor visitor = new MixinClassVisitor(); 67 | reader.accept(visitor, ClassReader.SKIP_FRAMES); 68 | MixinInfo info = visitor.getInfo(); 69 | ClassReader targetReader = mixinEntry.targetReader(classLoaders); 70 | 71 | ClassNode targetNode; 72 | TargetedMixin writerEntry = writers.get(mixinEntry.getTargetClass()); 73 | if (writerEntry == null) { 74 | ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES); 75 | targetNode = new ClassNode(); 76 | targetReader.accept(targetNode, ClassReader.SKIP_FRAMES); 77 | writers.put(mixinEntry.getTargetClass(), new TargetedMixin(writer, mixinEntry.getClassLoader(), targetNode)); 78 | } else { 79 | targetNode = writerEntry.node; 80 | } 81 | for (MixinsEditor editor : editors) { 82 | editor.edit(targetNode, info); 83 | } 84 | } 85 | for (Entry writerEntry : writers.entrySet()) { 86 | TargetedMixin mixin = writerEntry.getValue(); 87 | mixin.node.accept(mixin.writer); 88 | try { 89 | TunaClassDefiner.defineClass( 90 | writerEntry.getKey(), 91 | mixin.writer.toByteArray(), 92 | config.getNeighbor(writerEntry.getKey()), 93 | mixin.classLoader, 94 | null 95 | ); 96 | } catch (Throwable throwable) { 97 | if (!ignoreLoadedClasses) { 98 | if (throwable.getClass() == LinkageError.class || Objects.equals(throwable.getCause().getClass(), LinkageError.class)) { 99 | throw new IllegalStateException("Class " + writerEntry.getKey() + " has already been loaded."); 100 | } 101 | throw new IllegalStateException("Unable to load mixin modifications for class " + writerEntry.getKey(), throwable); 102 | } 103 | } 104 | } 105 | } 106 | 107 | @AllArgsConstructor 108 | private static class TargetedMixin { 109 | 110 | private final ClassWriter writer; 111 | private final ClassLoader classLoader; 112 | private final ClassNode node; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/ap/MixinsProcessor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.ap; 2 | 3 | import io.tunabytes.Mixin; 4 | 5 | import javax.annotation.processing.AbstractProcessor; 6 | import javax.annotation.processing.RoundEnvironment; 7 | import javax.annotation.processing.SupportedAnnotationTypes; 8 | import javax.annotation.processing.SupportedSourceVersion; 9 | import javax.lang.model.SourceVersion; 10 | import javax.lang.model.element.Element; 11 | import javax.lang.model.element.PackageElement; 12 | import javax.lang.model.element.TypeElement; 13 | import javax.lang.model.type.MirroredTypeException; 14 | import javax.lang.model.type.TypeMirror; 15 | import javax.lang.model.util.Types; 16 | import javax.tools.FileObject; 17 | import javax.tools.JavaFileObject; 18 | import javax.tools.StandardLocation; 19 | import java.io.IOException; 20 | import java.io.Writer; 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | import java.util.Set; 24 | import java.util.StringJoiner; 25 | import java.util.stream.Collectors; 26 | 27 | /** 28 | * An annotation processor for generating mixins.properties and 29 | * mixins-neighbors.properties. 30 | */ 31 | @SupportedAnnotationTypes("io.tunabytes.Mixin") 32 | @SupportedSourceVersion(SourceVersion.RELEASE_8) 33 | public class MixinsProcessor extends AbstractProcessor { 34 | 35 | private final StringJoiner mixins = new StringJoiner(System.lineSeparator()); 36 | private final Map neighbors = new HashMap<>(); 37 | 38 | @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { 39 | for (Element element : roundEnv.getElementsAnnotatedWith(Mixin.class)) { 40 | TypeElement type = (TypeElement) element; 41 | Mixin mixin = type.getAnnotation(Mixin.class); 42 | String packageName = ""; 43 | try { 44 | if (!mixin.name().isEmpty()) { 45 | packageName = mixin.name().substring(0, mixin.name().lastIndexOf('.')); 46 | mixins.add(type.getQualifiedName().toString() + "=" + mixin.name()); 47 | } else { 48 | mixin.value(); 49 | } 50 | } catch (MirroredTypeException e) { 51 | if (mixin.name().isEmpty() && e.getTypeMirror().toString().equals("java.lang.Object")) { 52 | throw new IllegalStateException("@Mixin on " + type.getQualifiedName() + " does not specify a class name() or Class value()"); 53 | } 54 | mixins.add(type.getQualifiedName().toString() + "=" + e.getTypeMirror()); 55 | PackageElement packageElement = (PackageElement) asTypeElement(e.getTypeMirror()).getEnclosingElement(); 56 | packageName = packageElement.toString(); 57 | } 58 | if (packageName.startsWith("java.")) { 59 | throw new IllegalArgumentException("Cannot add @Mixins on Java classes!"); 60 | } 61 | String className = ("Neighbor" + packageName.hashCode()).replace("-", ""); 62 | String pn = packageName + "." + className; 63 | if (neighbors.get(packageName) == null) { 64 | try { 65 | String fileName = packageName.isEmpty() 66 | ? className 67 | : pn; 68 | neighbors.put(packageName, fileName); 69 | JavaFileObject filerSourceFile = processingEnv.getFiler().createSourceFile(fileName); 70 | try (Writer writer = filerSourceFile.openWriter()) { 71 | if (!packageName.isEmpty()) { 72 | writer.write("package " + packageName + ";"); 73 | writer.write("\n"); 74 | } 75 | writer.write("class " + className + " {}"); 76 | } catch (Exception i) { 77 | try { 78 | filerSourceFile.delete(); 79 | } catch (Exception ignored) { 80 | } 81 | throw i; 82 | } 83 | } catch (IOException ignored) { 84 | } 85 | } 86 | } 87 | try { 88 | FileObject object = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "mixins.properties"); 89 | try (Writer writer = object.openWriter()) { 90 | writer.write(mixins.toString()); 91 | } 92 | } catch (IOException ignored) { 93 | } 94 | try { 95 | FileObject object = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "mixins-neighbors.properties"); 96 | try (Writer writer = object.openWriter()) { 97 | writer.write(neighbors.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("\n"))); 98 | } 99 | } catch (IOException ignored) {} 100 | return false; 101 | } 102 | 103 | private TypeElement asTypeElement(TypeMirror typeMirror) { 104 | Types TypeUtils = processingEnv.getTypeUtils(); 105 | return (TypeElement) TypeUtils.asElement(typeMirror); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/editor/AccessorEditor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.editor; 2 | 3 | import io.tunabytes.bytecode.introspect.MixinInfo; 4 | import io.tunabytes.bytecode.introspect.MixinMethod; 5 | import io.tunabytes.bytecode.introspect.MixinMethod.CallType; 6 | import lombok.SneakyThrows; 7 | import org.objectweb.asm.Type; 8 | import org.objectweb.asm.tree.*; 9 | 10 | /** 11 | * A mixins editor for processing {@link io.tunabytes.Accessor} methods. 12 | */ 13 | public class AccessorEditor implements MixinsEditor { 14 | 15 | @SneakyThrows @Override public void edit(ClassNode node, MixinInfo info) { 16 | if (info.isMixinInterface()) { 17 | node.interfaces.add(info.getMixinInternalName()); 18 | for (MixinMethod method : info.getMethods()) { 19 | if (!method.isAccessor()) continue; 20 | if ((method.getMethodNode().access & ACC_ABSTRACT) == 0) { 21 | throw new IllegalArgumentException("@Accessor cannot be used on non-abstract methods! (" + node.name + " in " + info.getMixinName() + ")"); 22 | } 23 | MethodNode n = method.getMethodNode(); 24 | MethodNode impl = new MethodNode(ACC_PUBLIC, method.getName(), method.getDescriptor().getDescriptor(), n.signature, n.exceptions.toArray(new String[0])); 25 | Type targetType = Type.getMethodType(n.desc); 26 | Type returnType = targetType.getReturnType(); 27 | Type[] arguments = targetType.getArgumentTypes(); 28 | if (method.getType() == CallType.GET) 29 | targetType = targetType.getReturnType(); 30 | else if (method.getType() == CallType.SET) 31 | targetType = arguments[0]; 32 | 33 | boolean isStatic; 34 | switch (method.getType()) { 35 | case GET: 36 | FieldNode accessed = node.fields.stream().filter(c -> c.name.equals(method.getAccessedProperty())).findFirst() 37 | .orElseThrow(() -> new NoSuchFieldException(method.getAccessedProperty())); 38 | isStatic = (accessed.access & ACC_STATIC) != 0; 39 | if (!isStatic) 40 | impl.instructions.add(new VarInsnNode(ALOAD, 0)); 41 | impl.instructions.add(new FieldInsnNode(GETFIELD, node.name, method.getAccessedProperty(), targetType.getDescriptor())); 42 | impl.instructions.add(new InsnNode(targetType.getOpcode(IRETURN))); 43 | break; 44 | case SET: 45 | accessed = node.fields.stream().filter(c -> c.name.equals(method.getAccessedProperty())).findFirst() 46 | .orElseThrow(() -> new NoSuchFieldException(method.getAccessedProperty())); 47 | isStatic = (accessed.access & ACC_STATIC) != 0; 48 | if (!isStatic) 49 | impl.instructions.add(new VarInsnNode(ALOAD, 0)); 50 | if ((accessed.access & ACC_FINAL) != 0) 51 | accessed.access &= ~ACC_FINAL; 52 | impl.instructions.add(new VarInsnNode(targetType.getOpcode(ILOAD), getArgIndex(0, isStatic, arguments))); 53 | impl.instructions.add(new FieldInsnNode(PUTFIELD, node.name, method.getAccessedProperty(), targetType.getDescriptor())); 54 | impl.instructions.add(new InsnNode(returnType.getOpcode(IRETURN))); 55 | break; 56 | case INVOKE: 57 | MethodNode accessedMethod = node.methods.stream().filter(m -> m.name.equals(method.getAccessedProperty()) && m.desc.equals(n.desc)) 58 | .findFirst().orElseThrow(() -> new NoSuchMethodException(method.getAccessedProperty())); 59 | isStatic = (accessedMethod.access & ACC_STATIC) != 0; 60 | if (!isStatic) 61 | impl.instructions.add(new VarInsnNode(ALOAD, 0)); 62 | for (int i = 0; i < targetType.getArgumentTypes().length; i++) { 63 | impl.instructions.add(new VarInsnNode(arguments[i].getOpcode(ILOAD), getArgIndex(i, isStatic, arguments))); 64 | } 65 | if (isStatic) 66 | impl.instructions.add(new MethodInsnNode(INVOKESTATIC, node.name, method.getAccessedProperty(), method.getDescriptor().getDescriptor())); 67 | else { 68 | if (method.isPrivate() || accessedMethod.name.endsWith("init>")) 69 | impl.instructions.add(new MethodInsnNode(INVOKESPECIAL, node.name, method.getAccessedProperty(), method.getDescriptor().getDescriptor())); 70 | else 71 | impl.instructions.add(new MethodInsnNode(INVOKEVIRTUAL, node.name, method.getAccessedProperty(), method.getDescriptor().getDescriptor())); 72 | } 73 | impl.instructions.add(new InsnNode(targetType.getReturnType().getOpcode(IRETURN))); 74 | break; 75 | } 76 | node.methods.add(impl); 77 | } 78 | } 79 | } 80 | 81 | private int getArgIndex(final int arg, boolean isStatic, Type[] args) { 82 | int index = isStatic ? 0 : 1; 83 | for (int i = 0; i < arg; i++) { 84 | index += args[i].getSize(); 85 | } 86 | return index; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/bytecode/introspect/MixinMethodVisitor.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.bytecode.introspect; 2 | 3 | import io.tunabytes.*; 4 | import io.tunabytes.Inject.At; 5 | import io.tunabytes.bytecode.introspect.MixinMethod.CallType; 6 | import org.objectweb.asm.AnnotationVisitor; 7 | import org.objectweb.asm.MethodVisitor; 8 | import org.objectweb.asm.Opcodes; 9 | import org.objectweb.asm.Type; 10 | import org.objectweb.asm.tree.MethodNode; 11 | 12 | public class MixinMethodVisitor extends MethodVisitor { 13 | 14 | private static final String OVERWRITE = Type.getDescriptor(Overwrite.class); 15 | private static final String INJECT = Type.getDescriptor(Inject.class); 16 | private static final String ACCESSOR = Type.getDescriptor(Accessor.class); 17 | private static final String MIRROR = Type.getDescriptor(Mirror.class); 18 | private static final String DEFINALIZE = Type.getDescriptor(Definalize.class); 19 | private static final String ACTUAL_TYPE = Type.getDescriptor(ActualType.class); 20 | 21 | protected MethodNode node; 22 | protected String mirrorName; 23 | protected Type returnType; 24 | protected Type[] argumentTypes; 25 | protected boolean overwrite, inject, accessor, mirror, definalize, remap; 26 | protected String overwrittenName; 27 | protected String injectMethodName; 28 | protected String accessorName; 29 | protected int injectLine; 30 | protected At injectAt; 31 | protected CallType type = CallType.INVOKE; 32 | 33 | public MixinMethodVisitor(MethodNode node) { 34 | super(Opcodes.ASM8, node); 35 | this.node = node; 36 | Type desc = Type.getMethodType(node.desc); 37 | returnType = desc.getReturnType(); 38 | argumentTypes = desc.getArgumentTypes(); 39 | } 40 | 41 | @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { 42 | boolean visitingAccessor = ACCESSOR.equals(descriptor); 43 | boolean visitingOverwrite = OVERWRITE.equals(descriptor); 44 | boolean visitingInject = INJECT.equals(descriptor); 45 | boolean visitingMirror = MIRROR.equals(descriptor); 46 | boolean visitingActualType = ACTUAL_TYPE.equals(descriptor); 47 | if (visitingOverwrite) 48 | overwrite = true; 49 | if (visitingInject) 50 | inject = true; 51 | if (visitingAccessor) 52 | accessor = true; 53 | if (visitingMirror) 54 | mirror = true; 55 | if (DEFINALIZE.equals(descriptor)) 56 | definalize = true; 57 | return new AnnotationVisitor(Opcodes.ASM8, super.visitAnnotation(descriptor, visible)) { 58 | @Override public void visit(String name, Object value) { 59 | super.visit(name, value); 60 | if (visitingAccessor && name.equals("value")) { 61 | accessorName = (String) value; 62 | } 63 | if (visitingOverwrite && name.equals("value")) { 64 | overwrittenName = (String) value; 65 | } 66 | if (visitingMirror && name.equals("value")) { 67 | mirrorName = (String) value; 68 | } 69 | if (visitingActualType && name.equals("value")) { 70 | remap = true; 71 | String rtype = returnType.getDescriptor(); 72 | returnType = fromActualType(rtype, (String) value); 73 | } 74 | if (visitingInject) { 75 | switch (name) { 76 | case "method": { 77 | injectMethodName = (String) value; 78 | break; 79 | } 80 | case "lineNumber": { 81 | injectLine = (int) value; 82 | break; 83 | } 84 | } 85 | } 86 | } 87 | 88 | @Override public void visitEnum(String name, String descriptor, String value) { 89 | super.visitEnum(name, descriptor, value); 90 | if (visitingInject && name.equals("at")) { 91 | injectAt = At.at(value); 92 | } 93 | } 94 | }; 95 | } 96 | 97 | @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { 98 | return new AnnotationVisitor(Opcodes.ASM8, super.visitParameterAnnotation(parameter, descriptor, visible)) { 99 | @Override public void visit(String name, Object value) { 100 | remap = true; 101 | if (ACTUAL_TYPE.equals(descriptor)) 102 | argumentTypes[parameter] = fromActualType(argumentTypes[parameter].getDescriptor(), (String) value); 103 | super.visit(name, value); 104 | } 105 | }; 106 | } 107 | 108 | protected String getActualName(String accessorName) { 109 | if (accessorName.startsWith("get")) { 110 | type = CallType.GET; 111 | return normalize("get", accessorName); 112 | } 113 | if (accessorName.startsWith("set")) { 114 | type = CallType.SET; 115 | return normalize("set", accessorName); 116 | } 117 | if (accessorName.startsWith("is")) { 118 | type = CallType.GET; 119 | return normalize("is", accessorName); 120 | } 121 | if (accessorName.startsWith("call")) { 122 | type = CallType.INVOKE; 123 | return normalize("call", accessorName); 124 | } 125 | if (accessorName.startsWith("invoke")) { 126 | type = CallType.INVOKE; 127 | return normalize("invoke", accessorName); 128 | } 129 | return accessorName; 130 | } 131 | 132 | private static String normalize(String prefix, String value) { 133 | if (value.length() > prefix.length()) { 134 | return Character.toLowerCase(value.charAt(prefix.length())) + value.substring(prefix.length() + 1); 135 | } 136 | return value; 137 | } 138 | 139 | public static Type fromActualType(String descriptor, String actualType) { 140 | String arrayAddition = ""; 141 | if (descriptor.startsWith("[")) 142 | arrayAddition = descriptor.substring(0, descriptor.lastIndexOf('[') + 1); 143 | return Type.getType(arrayAddition + "L" + actualType.replace('.', '/') + ";"); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /core/src/main/java/io/tunabytes/classloader/SecurityActions.java: -------------------------------------------------------------------------------- 1 | package io.tunabytes.classloader; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.reflect.AccessibleObject; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.Field; 8 | import java.lang.reflect.Method; 9 | import java.security.AccessController; 10 | import java.security.PrivilegedAction; 11 | import java.security.PrivilegedActionException; 12 | import java.security.PrivilegedExceptionAction; 13 | import java.util.*; 14 | 15 | class SecurityActions extends SecurityManager { 16 | 17 | public static final SecurityActions stack = new SecurityActions(); 18 | 19 | /** 20 | * Since Java 9 abruptly removed Reflection.getCallerClass() 21 | * in favour of StackWalker we are left having to find a 22 | * solution for the older versions without upsetting the new compiler. 23 | *

24 | * The member scoped function getClassContext() 25 | * available as a SecurityManager sibling remains 26 | * functional across all versions, for now. 27 | * 28 | * @return represents the declaring class of the method that invoked 29 | * the method that called this or index 2 on the stack trace. 30 | * @since 3.23 31 | */ 32 | public Class getCallerClass() { 33 | return getClassContext()[2]; 34 | } 35 | 36 | static Method[] getDeclaredMethods(final Class clazz) { 37 | if (System.getSecurityManager() == null) 38 | return clazz.getDeclaredMethods(); 39 | else { 40 | return AccessController.doPrivileged((PrivilegedAction) clazz::getDeclaredMethods); 41 | } 42 | } 43 | 44 | static Constructor[] getDeclaredConstructors(final Class clazz) { 45 | if (System.getSecurityManager() == null) 46 | return clazz.getDeclaredConstructors(); 47 | else { 48 | return AccessController.doPrivileged((PrivilegedAction[]>) clazz::getDeclaredConstructors); 49 | } 50 | } 51 | 52 | static MethodHandle getMethodHandle(final Class clazz, final String name, final Class[] params) throws NoSuchMethodException { 53 | try { 54 | return AccessController.doPrivileged( 55 | (PrivilegedExceptionAction) () -> { 56 | Method rmet = clazz.getDeclaredMethod(name, params); 57 | rmet.setAccessible(true); 58 | MethodHandle meth = MethodHandles.lookup().unreflect(rmet); 59 | rmet.setAccessible(false); 60 | return meth; 61 | }); 62 | } catch (PrivilegedActionException e) { 63 | if (e.getCause() instanceof NoSuchMethodException) 64 | throw (NoSuchMethodException) e.getCause(); 65 | throw new RuntimeException(e.getCause()); 66 | } 67 | } 68 | 69 | static Method getDeclaredMethod(final Class clazz, final String name, 70 | final Class[] types) throws NoSuchMethodException { 71 | if (System.getSecurityManager() == null) 72 | return clazz.getDeclaredMethod(name, types); 73 | else { 74 | try { 75 | return AccessController.doPrivileged( 76 | (PrivilegedExceptionAction) () -> clazz.getDeclaredMethod(name, types)); 77 | } catch (PrivilegedActionException e) { 78 | if (e.getCause() instanceof NoSuchMethodException) 79 | throw (NoSuchMethodException) e.getCause(); 80 | 81 | throw new RuntimeException(e.getCause()); 82 | } 83 | } 84 | } 85 | 86 | static Constructor getDeclaredConstructor(final Class clazz, 87 | final Class[] types) 88 | throws NoSuchMethodException { 89 | if (System.getSecurityManager() == null) 90 | return clazz.getDeclaredConstructor(types); 91 | else { 92 | try { 93 | return AccessController.doPrivileged( 94 | (PrivilegedExceptionAction>) () -> clazz.getDeclaredConstructor(types)); 95 | } catch (PrivilegedActionException e) { 96 | if (e.getCause() instanceof NoSuchMethodException) 97 | throw (NoSuchMethodException) e.getCause(); 98 | 99 | throw new RuntimeException(e.getCause()); 100 | } 101 | } 102 | } 103 | 104 | static void setAccessible(final AccessibleObject ao, 105 | final boolean accessible) { 106 | if (System.getSecurityManager() == null) 107 | ao.setAccessible(accessible); 108 | else { 109 | AccessController.doPrivileged((PrivilegedAction) () -> { 110 | ao.setAccessible(accessible); 111 | return null; 112 | }); 113 | } 114 | } 115 | 116 | static void set(final Field fld, final Object target, final Object value) 117 | throws IllegalAccessException { 118 | if (System.getSecurityManager() == null) 119 | fld.set(target, value); 120 | else { 121 | try { 122 | AccessController.doPrivileged( 123 | (PrivilegedExceptionAction) () -> { 124 | fld.set(target, value); 125 | return null; 126 | }); 127 | } catch (PrivilegedActionException e) { 128 | if (e.getCause() instanceof NoSuchMethodException) 129 | throw (IllegalAccessException) e.getCause(); 130 | throw new RuntimeException(e.getCause()); 131 | } 132 | } 133 | } 134 | 135 | static TheUnsafe getSunMiscUnsafeAnonymously() throws ClassNotFoundException { 136 | try { 137 | return AccessController.doPrivileged( 138 | (PrivilegedExceptionAction) () -> { 139 | Class unsafe = Class.forName("sun.misc.Unsafe"); 140 | Field theUnsafe = unsafe.getDeclaredField("theUnsafe"); 141 | theUnsafe.setAccessible(true); 142 | TheUnsafe usf = new TheUnsafe(unsafe, theUnsafe.get(null)); 143 | theUnsafe.setAccessible(false); 144 | disableWarning(usf); 145 | return usf; 146 | }); 147 | } catch (PrivilegedActionException e) { 148 | if (e.getCause() instanceof ClassNotFoundException) 149 | throw (ClassNotFoundException) e.getCause(); 150 | if (e.getCause() instanceof NoSuchFieldException) 151 | throw new ClassNotFoundException("No such instance.", e.getCause()); 152 | if (e.getCause() instanceof IllegalAccessException 153 | || e.getCause() instanceof IllegalAccessException 154 | || e.getCause() instanceof SecurityException) 155 | throw new ClassNotFoundException("Security denied access.", e.getCause()); 156 | throw new RuntimeException(e.getCause()); 157 | } 158 | } 159 | 160 | /** 161 | * _The_ Notorious sun.misc.Unsafe in all its glory, but anonymous 162 | * so as not to attract unwanted attention. Kept in two separate 163 | * parts it manages to avoid detection from linker/compiler/general 164 | * complainers and those. This functionality will vanish from the 165 | * JDK soon but in the meantime it shouldn't be an obstacle. 166 | *

167 | * All exposed methods are cached in a dictionary with overloaded 168 | * methods collected under their corresponding keys. Currently the 169 | * implementation assumes there is only one, if you need find a 170 | * need there will have to be a compare. 171 | * 172 | * @since 3.23 173 | */ 174 | static class TheUnsafe { 175 | 176 | final Class unsafe; 177 | final Object theUnsafe; 178 | final Map> methods = new HashMap<>(); 179 | 180 | TheUnsafe(Class c, Object o) { 181 | unsafe = c; 182 | theUnsafe = o; 183 | for (Method m : unsafe.getDeclaredMethods()) { 184 | if (!methods.containsKey(m.getName())) { 185 | methods.put(m.getName(), Collections.singletonList(m)); 186 | continue; 187 | } 188 | if (methods.get(m.getName()).size() == 1) 189 | methods.put(m.getName(), new ArrayList<>(methods.get(m.getName()))); 190 | methods.get(m.getName()).add(m); 191 | } 192 | } 193 | 194 | private Method getM(String name) { 195 | return methods.get(name).get(0); 196 | } 197 | 198 | public Object call(String name, Object... args) { 199 | try { 200 | return getM(name).invoke(theUnsafe, args); 201 | } catch (Throwable t) {t.printStackTrace();} 202 | return null; 203 | } 204 | } 205 | 206 | /** 207 | * Java 9 now complains about every privileged action regardless. 208 | * Displaying warnings of "illegal usage" and then instructing users 209 | * to go hassle the maintainers in order to have it fixed. 210 | * Making it hush for now, see all fixed. 211 | * 212 | * @param tu theUnsafe that'll fix it 213 | */ 214 | static void disableWarning(TheUnsafe tu) { 215 | try { 216 | if (TunaClassDefiner.MAJOR_VERSION < TunaClassDefiner.JAVA_9) 217 | return; 218 | Class cls = Class.forName("jdk.internal.module.IllegalAccessLogger"); 219 | Field logger = cls.getDeclaredField("logger"); 220 | tu.call("putObjectVolatile", cls, tu.call("staticFieldOffset", logger), null); 221 | } catch (Exception e) { /*swallow*/ } 222 | } 223 | } 224 | 225 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. --------------------------------------------------------------------------------