diff --git a/gradle.properties b/gradle.properties index 319af4dcf..528f6c416 100644 --- a/gradle.properties +++ b/gradle.properties @@ -106,7 +106,7 @@ debug_woot = false # SECTION: custom injected tags -groovy_version = 4.0.21 +groovy_version = 4.0.26 # END SECTION: custom injected tags diff --git a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java index dce5ebb97..0499deded 100644 --- a/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java +++ b/src/main/java/com/cleanroommc/groovyscript/GroovyScript.java @@ -101,7 +101,6 @@ public class GroovyScript { @Mod.EventHandler public void onConstruction(FMLConstructionEvent event) { - JavaVersionCheck.validateJavaVersion(event.getSide()); if (!SandboxData.isInitialised()) { LOGGER.throwing(new IllegalStateException("Sandbox data should have been initialised by now, but isn't! Trying to initialize again.")); SandboxData.initialize((File) FMLInjectionData.data()[6], LOGGER); diff --git a/src/main/java/com/cleanroommc/groovyscript/IncompatibleJavaException.java b/src/main/java/com/cleanroommc/groovyscript/IncompatibleJavaException.java deleted file mode 100644 index 115c30837..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/IncompatibleJavaException.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.cleanroommc.groovyscript; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.GuiErrorScreen; -import net.minecraftforge.fml.client.CustomModLoadingErrorDisplayException; -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; - -import java.util.List; - -@SideOnly(Side.CLIENT) -public class IncompatibleJavaException extends CustomModLoadingErrorDisplayException { - - private final String msg; - - public IncompatibleJavaException(String msg) { - this.msg = msg; - } - - @Override - public void initGui(GuiErrorScreen errorScreen, FontRenderer fontRenderer) {} - - @Override - public void drawScreen(GuiErrorScreen errorScreen, FontRenderer fontRenderer, int mouseRelX, int mouseRelY, float tickTime) { - List lines = fontRenderer.listFormattedStringToWidth(this.msg, errorScreen.width - 40); - int buttonSpace = 20 + 2 * 18; // button is 18 pixel high + 18 pixel margin on both sides - int y = (errorScreen.height - buttonSpace) / 2 - (fontRenderer.FONT_HEIGHT * 2 + 1) / 2; // font height * 2 / - for (String line : lines) { - int width = fontRenderer.getStringWidth(line); - int x = errorScreen.width / 2 - width / 2; - fontRenderer.drawStringWithShadow(line, x, y, 0xFFFFFFFF); - y += fontRenderer.FONT_HEIGHT; - } - } -} diff --git a/src/main/java/com/cleanroommc/groovyscript/JavaVersionCheck.java b/src/main/java/com/cleanroommc/groovyscript/JavaVersionCheck.java deleted file mode 100644 index e078c2ef4..000000000 --- a/src/main/java/com/cleanroommc/groovyscript/JavaVersionCheck.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.cleanroommc.groovyscript; - -import net.minecraftforge.fml.relauncher.Side; -import net.minecraftforge.fml.relauncher.SideOnly; - -/** - * Checks that the Java version being used can be used by the Groovy that GroovyScript uses. - * Our version of Groovy is currently not compatible with java versions above 21. - */ -public class JavaVersionCheck { - - private static final int MAXIMUM_VERSION = 21; - - /** - * Checks that the Java version being used can run GroovyScript scripts. - */ - public static void validateJavaVersion(Side side) { - int version = getJavaVersion(); - if (version > MAXIMUM_VERSION) handleJavaVersionException(version, side); - } - - private static void handleJavaVersionException(int version, Side side) { - String msg1 = "GroovyScript's version of Groovy does not work with Java versions greater than " + MAXIMUM_VERSION + " currently."; - String msg2 = "Please downgrade to Java " + MAXIMUM_VERSION + " or lower. Your current Java version is " + version + "."; - if (side.isClient()) { - throwIncompatibleJavaException(msg1 + "\n" + msg2); - } else { - throw new IllegalStateException(msg1 + " " + msg2); - } - } - - /** - * Because the super class of this exception is client only (since the screen only works on client) - * this has to be in a separate method. - */ - @SideOnly(Side.CLIENT) - private static void throwIncompatibleJavaException(String msg) { - throw new IncompatibleJavaException(msg); - } - - /** - * Gets the major version of Java currently running. - * - * - * - * - * - * - * - * - * - * - * - *
1.8.0_51=8
21.0.6=21
- * Code comes from - * Stack Overflow. - */ - private static int getJavaVersion() { - String version = System.getProperty("java.version"); - if (version.startsWith("1.")) { - version = version.substring(2, 3); - } else { - int dot = version.indexOf("."); - if (dot != -1) version = version.substring(0, dot); - } - return Integer.parseInt(version); - } -} diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/LoaderControllerMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/LoaderControllerMixin.java index 289402fb2..6a0ee9708 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/LoaderControllerMixin.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/LoaderControllerMixin.java @@ -2,16 +2,13 @@ import com.cleanroommc.groovyscript.GroovyScript; import com.cleanroommc.groovyscript.sandbox.LoadStage; -import net.minecraftforge.fml.client.CustomModLoadingErrorDisplayException; import net.minecraftforge.fml.common.LoadController; import net.minecraftforge.fml.common.LoaderState; -import net.minecraftforge.fml.common.ModContainer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.lang.reflect.InvocationTargetException; @Mixin(value = LoadController.class, remap = false) public class LoaderControllerMixin { @@ -28,15 +25,4 @@ public void preInit(LoaderState state, Object[] eventData, CallbackInfo ci) { GroovyScript.runGroovyScriptsInLoader(LoadStage.POST_INIT); } } - - @Inject(method = "errorOccurred", at = @At(value = "NEW", target = "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)Lorg/apache/logging/log4j/message/FormattedMessage;", shift = At.Shift.BEFORE)) - public void errorOccured(ModContainer modContainer, Throwable exception, CallbackInfo ci) throws Throwable { - if (exception instanceof InvocationTargetException) { - exception = exception.getCause(); - } - if (exception instanceof CustomModLoadingErrorDisplayException) { - // normally every exception gets wrapped in a LoaderException making these """custom""" exceptions useless, thanks cpw - throw exception; - } - } } diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/AsmDecompilerMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/AsmDecompilerMixin.java index 23cfea5b7..818b7a8be 100644 --- a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/AsmDecompilerMixin.java +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/AsmDecompilerMixin.java @@ -1,76 +1,20 @@ package com.cleanroommc.groovyscript.core.mixin.groovy; -import com.cleanroommc.groovyscript.sandbox.transformer.AsmDecompileHelper; -import groovy.lang.GroovyRuntimeException; import org.codehaus.groovy.ast.decompiled.AsmDecompiler; import org.codehaus.groovy.ast.decompiled.ClassStub; -import org.codehaus.groovy.util.URLStreams; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.tree.ClassNode; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.SoftReference; -import java.lang.reflect.InvocationTargetException; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; -import java.util.Map; @Mixin(value = AsmDecompiler.class, remap = false) public class AsmDecompilerMixin { - @Shadow - @Final - private static Map> stubCache; - - @Inject(method = "parseClass", at = @At("HEAD"), cancellable = true) + @Inject(method = "parseClass", at = @At("HEAD")) private static void parseClass(URL url, CallbackInfoReturnable cir) { - URI uri; - try { - uri = url.toURI(); - } catch (URISyntaxException e) { - throw new GroovyRuntimeException(e); - } - - SoftReference ref = stubCache.get(uri); - ClassStub stub = (ref != null ? ref.get() : null); - if (stub == null) { - try (InputStream stream = new BufferedInputStream(URLStreams.openUncachedStream(url))) { - ClassReader classReader = new ClassReader(stream); - ClassNode classNode = new ClassNode(); - classReader.accept(classNode, 0); - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); - classNode.accept(writer); - byte[] bytes = writer.toByteArray(); - if (!AsmDecompileHelper.remove(classNode.visibleAnnotations, AsmDecompileHelper.SIDE)) { - bytes = AsmDecompileHelper.transform(classNode.name, bytes); - } - - // now decompile the class normally - groovyjarjarasm.asm.ClassReader classReader2 = new groovyjarjarasm.asm.ClassReader(bytes); - groovyjarjarasm.asm.ClassVisitor decompiler = AsmDecompileHelper.makeGroovyDecompiler(); - classReader2.accept(decompiler, ClassReader.SKIP_FRAMES); - stub = AsmDecompileHelper.getDecompiledClass(decompiler); - stubCache.put(uri, new SoftReference<>(stub)); - } catch (IOException | - ClassNotFoundException | - NoSuchFieldException | - NoSuchMethodException | - IllegalAccessException | - InvocationTargetException | - InstantiationException e) { - throw new RuntimeException(e); - } - } - cir.setReturnValue(stub); + // redirected in ClassNodeResolverMixin + throw new UnsupportedOperationException(); } } diff --git a/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ClassNodeResolverMixin.java b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ClassNodeResolverMixin.java new file mode 100644 index 000000000..2875922e3 --- /dev/null +++ b/src/main/java/com/cleanroommc/groovyscript/core/mixin/groovy/ClassNodeResolverMixin.java @@ -0,0 +1,60 @@ +package com.cleanroommc.groovyscript.core.mixin.groovy; + +import com.cleanroommc.groovyscript.sandbox.transformer.AsmDecompileHelper; +import groovy.lang.GroovyClassLoader; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.decompiled.AsmReferenceResolver; +import org.codehaus.groovy.ast.decompiled.ClassStub; +import org.codehaus.groovy.ast.decompiled.DecompiledClassNode; +import org.codehaus.groovy.control.ClassNodeResolver; +import org.codehaus.groovy.control.CompilationUnit; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(value = ClassNodeResolver.class, remap = false) +public abstract class ClassNodeResolverMixin { + + @Shadow + private static boolean isFromAnotherClassLoader(GroovyClassLoader loader, String fileName) { + return false; + } + + @Shadow + private static ClassNodeResolver.LookupResult tryAsScript(String name, CompilationUnit compilationUnit, ClassNode oldClass) { + return null; + } + + /** + * @author brachy + * @reason properly find classes + */ + @Overwrite + private ClassNodeResolver.LookupResult findDecompiled(final String name, final CompilationUnit compilationUnit, final GroovyClassLoader loader) { + ClassNode node = ClassHelper.make(name); + if (node.isResolved()) { + return new ClassNodeResolver.LookupResult(null, node); + } + + DecompiledClassNode asmClass = null; + ClassStub stub = AsmDecompileHelper.findDecompiledClass(name); + if (stub != null) { + asmClass = new DecompiledClassNode(stub, new AsmReferenceResolver((ClassNodeResolver) (Object) this, compilationUnit)); + if (!asmClass.getName().equals(name)) { + // this may happen under Windows because getResource is case-insensitive under that OS! + asmClass = null; + } + } + + if (asmClass != null) { + String fileName = name.replace('.', '/') + ".class"; + if (isFromAnotherClassLoader(loader, fileName)) { + return tryAsScript(name, compilationUnit, asmClass); + } + + return new ClassNodeResolver.LookupResult(null, asmClass); + } + return null; + } +} diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java index fce4c9fb8..afa59e155 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/GroovyScriptClassLoader.java @@ -188,12 +188,6 @@ public Class loadClass(final String name, boolean lookupScriptFiles, boolean cls = recompile(source, name); } catch (IOException ioe) { last = new ClassNotFoundException("IOException while opening groovy source: " + name, ioe); - } finally { - if (cls == null) { - removeClassCacheEntry(name); - } else { - setClassCacheEntry(cls); - } } } diff --git a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/AsmDecompileHelper.java b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/AsmDecompileHelper.java index e0fee5136..df7b4667a 100644 --- a/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/AsmDecompileHelper.java +++ b/src/main/java/com/cleanroommc/groovyscript/sandbox/transformer/AsmDecompileHelper.java @@ -1,21 +1,26 @@ package com.cleanroommc.groovyscript.sandbox.transformer; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.launchwrapper.IClassTransformer; import net.minecraft.launchwrapper.Launch; import net.minecraftforge.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper; import net.minecraftforge.fml.relauncher.FMLLaunchHandler; import net.minecraftforge.fml.relauncher.SideOnly; import org.codehaus.groovy.ast.decompiled.ClassStub; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.Opcodes; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AnnotationNode; +import java.io.IOException; +import java.lang.ref.SoftReference; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; +import java.util.Map; + public class AsmDecompileHelper { @@ -26,6 +31,8 @@ public class AsmDecompileHelper { private static Constructor decompilerConstructor; private static Field resultField; + private static final Map> stubCache = new Object2ObjectOpenHashMap<>(); + static { transformerExceptions.add("javax."); transformerExceptions.add("argo."); @@ -52,6 +59,34 @@ public static ClassStub getDecompiledClass(groovyjarjarasm.asm.ClassVisitor clas return (ClassStub) resultField.get(classVisitor); } + /** + * Finds decompiled class bytes via forge class loader. + * This magically fixes the "class version 68" error. Which was caused by loading java classes on java > 23. + * This will return null for java classes. + */ + public static @Nullable ClassStub findDecompiledClass(String className) { + SoftReference ref = stubCache.get(className); + ClassStub stub = ref == null ? null : ref.get(); + if (stub != null) return stub; + try { + // TODO consider transformer exclusions + byte[] bytes = Launch.classLoader.getClassBytes(className); + if (bytes == null) return null; + bytes = transform(className, bytes); + groovyjarjarasm.asm.ClassReader classReader = new groovyjarjarasm.asm.ClassReader(bytes); + groovyjarjarasm.asm.ClassVisitor decompiler = makeGroovyDecompiler(); + classReader.accept(decompiler, ClassReader.SKIP_FRAMES); + stub = AsmDecompileHelper.getDecompiledClass(decompiler); + stubCache.put(className, new SoftReference<>(stub)); + } catch (IOException e) { + return null; + } catch (NoSuchFieldException | ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + return stub; + } + public static byte[] transform(String className, byte[] bytes) { for (String s : transformerExceptions) { if (className.startsWith(s)) { @@ -90,12 +125,14 @@ public static boolean remove(List anns, String side) { return false; } - public static class DecompileVisitor extends ClassVisitor { - - private ClassStub result; + public static short readClassVersion(byte[] classBytes) { + int offset = 6; + return (short) ((classBytes[offset] & 255) << 8 | classBytes[offset + 1] & 255); + } - public DecompileVisitor() { - super(Opcodes.ASM5); - } + public static void writeClassVersion(byte[] classBytes, short version) { + int offset = 6; + classBytes[offset] = (byte) ((version >> 8) & 255); + classBytes[offset + 1] = (byte) (version & 255); } } diff --git a/src/main/resources/mixin.groovyscript.json b/src/main/resources/mixin.groovyscript.json index 5ca5064f1..1c1a8e98a 100644 --- a/src/main/resources/mixin.groovyscript.json +++ b/src/main/resources/mixin.groovyscript.json @@ -23,6 +23,7 @@ "TileEntityPistonMixin", "VillagerProfessionAccessor", "groovy.AsmDecompilerMixin", + "groovy.ClassNodeResolverMixin", "groovy.ClosureMixin", "groovy.CompUnitClassGenMixin", "groovy.Java8Mixin",