diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1914186 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: ./gradlew jar + + - uses: actions/upload-artifact@v4 + with: + name: mcpc-java17-agent + path: build/libs/mcpc-java17-agent.jar + + - name: Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: build/libs/mcpc-java17-agent.jar + generate_release_notes: true diff --git a/README.md b/README.md index d0caacc..557821d 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,99 @@ -# MCPC+ Java 8 Agent +# MCPC+ Java 17 Agent -Lance un serveur **MCPC+ / Cauldron 1.6.4** sous **Java 8**, sans modifier le jar du serveur ni les mods. +Lance un serveur **MCPC+ / Cauldron 1.6.4** sous **Java 17**, sans modifier le jar du serveur ni les mods. -Ces serveurs refusent de démarrer sous Java 8, ou plantent pendant le chargement de Forge. Cet agent lève les trois blocages au démarrage. +Ces serveurs refusent de démarrer sur autre chose que Java 7, puis plantent pendant le chargement de launchwrapper et de Forge, car ils s'appuient sur des comportements de Java 7/8 qui n'existent plus en Java 17. Cet agent lève ces blocages au démarrage. ## Installation -1. Récupérez `mcpc-java8-agent.jar` depuis les [releases](../../releases), ou compilez-le (voir plus bas). +1. Récupérez `mcpc-java17-agent.jar` depuis les [releases](../../releases), ou compilez-le (voir plus bas). 2. Placez-le où vous voulez, par exemple à côté du jar du serveur. 3. Ajoutez `-javaagent:` à votre ligne de lancement, **avant** `-jar` : ```sh -java -javaagent:mcpc-java8-agent.jar -Xmx4G -jar mcpc.jar nogui +java -javaagent:mcpc-java17-agent.jar -Xmx4G -jar mcpc.jar nogui ``` -C'est tout. Aucun fichier de configuration, aucune modification du serveur ou des mods. +C'est tout. Aucun `--add-opens`, aucun fichier de configuration, aucune modification du serveur ou des mods. ### Exemple de start.sh ```sh #!/bin/sh -java -javaagent:mcpc-java8-agent.jar \ +java -javaagent:mcpc-java17-agent.jar \ -Xms2G -Xmx4G \ -jar mcpc.jar nogui ``` +### Options JVM à retirer + +Certaines options courantes des anciens scripts de lancement n'existent plus en Java 17 : + +- `-XX:+UseConcMarkSweepGC` et les options `CMS*` : supprimées, **la JVM refuse de démarrer**. Retirez-les (G1, le GC par défaut, convient très bien). +- `-XX:PermSize` / `-XX:MaxPermSize` : ignorées depuis Java 8, vous pouvez les retirer. + ### Vérifier que ça marche Au démarrage, la console affiche : ``` -[mcpc-j8] Java 8 detected (1.8.0_412), reporting 1.7.0_80 to bypass the MCPC+ version check. -[mcpc-j8] 4 class patch(es) loaded. -[mcpc-j8] Patched: net/minecraft/launchwrapper/Launch +[mcpc-j17] Java 17.0.x detected, reporting 1.7.0_80 to bypass the MCPC+ version check. +[mcpc-j17] Opened 741 JDK packages to the server. +[mcpc-j17] Reflection filters removed. +[mcpc-j17] 1 class patch(es) loaded. +[mcpc-j17] Patched: net/minecraft/launchwrapper/Launch +[mcpc-j17] ASM 4 detected, upgrading it to the bundled ASM. +[mcpc-j17] LaunchClassLoader delegates 241 platform packages to the JDK. +[mcpc-j17] Redirected sun.reflect in: net/minecraftforge/common/EnumHelper ``` -Si vous ne voyez aucune ligne `[mcpc-j8]`, l'agent n'est pas chargé : vérifiez que `-javaagent:` est bien **avant** `-jar` et que le chemin du jar est correct. +Sur un serveur dont `Launch` ou ASM sont déjà modernisés (voir plus bas), la ligne `Patched: .../Launch` est absente et `Recent ASM detected, keeping the server's ASM classes.` remplace la ligne sur ASM : c'est normal. + +Si vous ne voyez aucune ligne `[mcpc-j17]`, l'agent n'est pas chargé : vérifiez que `-javaagent:` est bien **avant** `-jar` et que le chemin du jar est correct. ## Ce que l'agent corrige -**1. Le refus explicite de Java 8.** `FMLLaunchHandler` teste `java.version` et arrête le serveur avec le message `Java 8 is not supported on MCPC+8.11! Please run MCPC+ using Java 7.` +**1. Le refus de toute version autre que Java 7.** `FMLLaunchHandler` teste `java.version` et arrête le serveur (`... is not supported on MCPC+8.11! Please run MCPC+ using Java 7.`). + +L'agent présente une version `1.7.x` à cette vérification (inoffensif si le serveur ne fait plus ce test). Passer `-Djava.version=1.7.0_80` sur la ligne de commande **ne fonctionne pas** : la JVM réécrit cette propriété pendant son initialisation. Il faut le faire depuis un agent, qui s'exécute après l'initialisation de la JVM mais avant le `main` du serveur. Un coremod ne peut pas corriger ce point : `FMLLaunchHandler` est exclu du `LaunchClassLoader`. + +**2. Le démarrage de launchwrapper.** `Launch` construit son class loader à partir de `(URLClassLoader) getClass().getClassLoader()`. Depuis Java 9, le class loader de l'application n'est plus un `URLClassLoader` : `ClassCastException` immédiate. La version corrigée de `Launch` lit le class path depuis `java.class.path`, comme les versions récentes de launchwrapper. + +La version corrigée garde aussi le correctif de la `ConcurrentModificationException` dans `Launch.launch` (liste des tweakers modifiée pendant son parcours). -L'agent présente une version `1.7.x` à cette vérification. Passer `-Djava.version=1.7.0_80` sur la ligne de commande **ne fonctionne pas** : la JVM réécrit cette propriété pendant son initialisation. Il faut le faire depuis un agent, qui s'exécute après l'initialisation de la JVM mais avant le `main` du serveur. +`Launch` n'est remplacé que s'il fait encore ce cast : un `Launch` déjà adapté par le serveur est conservé. -Un coremod ne peut pas corriger ce point : `FMLLaunchHandler` est explicitement exclu du `LaunchClassLoader`, donc hors de portée de tout `IClassTransformer`. +**3. Les classes du JDK hors de `java.base`.** Le `LaunchClassLoader` a pour parent le class loader de bootstrap. En Java 8, celui-ci voyait tout le JDK ; depuis Java 9, des modules comme `java.sql` ou `java.scripting` sont chargés par le *platform class loader*, et des classes comme `javax.sql.DataSource` (Ebean, plugins Bukkit) deviennent introuvables. L'agent fait déléguer ces packages au JDK. -**2. Un plantage dans launchwrapper.** `Launch.launch` parcourt la liste des tweakers tout en la modifiant, ce qui lève une `ConcurrentModificationException` sous Java 8. +**4. L'encapsulation des modules du JDK.** Forge, Bukkit et les mods appellent `setAccessible` sur des classes internes du JDK, ce que Java 17 refuse par défaut (`InaccessibleObjectException`). L'agent ouvre tous les packages du JDK au serveur, l'équivalent d'une longue liste de `--add-opens ...=ALL-UNNAMED`. Il retire aussi les filtres de réflexion ajoutés en Java 12 (qui cachent notamment `Field.modifiers`). -Si votre serveur utilise déjà launchwrapper 1.11 ou supérieur, ce point est déjà réglé de votre côté ; l'agent reste inoffensif. +**5. `sun.reflect.ReflectionFactory`, supprimé en Java 9.** L'`EnumHelper` de Forge s'en sert pour ajouter des valeurs aux enums (biomes, types de créatures, matériaux Bukkit de MCPC+...). Les références à `sun.reflect.ReflectionFactory`, `ConstructorAccessor` et `FieldAccessor` sont redirigées, dans toutes les classes chargées, vers des équivalents fournis par l'agent qui fonctionnent sous Java 17. -**3. Des plantages du déobfuscateur de Forge.** La version d'ASM embarquée dans ces serveurs échoue sur du bytecode produit par Java 8 (`Remapper`, `ClassReader`, `MethodInsnNode`). +**6. ASM 4.** MCPC+ est fourni avec ASM 4.1 (`libraries/org/ow2/asm/asm-all/4.1`), qui ne sait pas lire le bytecode Java 8+ (lambdas, méthodes static/default d'interface...) et ne connaît pas les API récentes (`Opcodes.ASM5` à `ASM9`) qu'utilisent les coremods modernes. + +L'agent embarque **ASM 9.7** et le substitue à ASM 4 au chargement. Rien à changer dans `libraries/`. Pour que Forge 1.6.4, MCPC+ et les mods écrits pour ASM 4 continuent de fonctionner, cet ASM 9.7 est adapté au build : + +- les classes `Remapping*Adapter`, supprimées après ASM 7.0 et utilisées par Forge, sont reprises d'ASM 7.0 ; +- `Type.getType(String)` accepte un nom interne comme le faisait ASM 4 (Forge s'en sert dans `EventTransformer`) ; +- les visiteurs déclarés avec l'API ASM 4 (le parseur de mods de FML, par exemple) ne lèvent plus `This feature requires ASM5` sur du bytecode récent : l'information est transmise au visiteur suivant, y compris le caractère « interface » des appels de méthodes, indispensable aux méthodes static d'interface. + +Un serveur qui fournit déjà un ASM 5 ou plus récent garde le sien. + +## Testé sur + +Sous OpenJDK 17.0.20, avec un mod de test compilé en Java 8 (lambdas, streams, méthodes static/default d'interface) et contenant un coremod qui utilise l'API `Opcodes.ASM9` : + +- MCPC+ 1.6.4 d'origine (Forge 9.11.1.965) avec son ASM 4.1 : le serveur démarre (`Done`), le mod est chargé et actif, sans aucune erreur dans les logs FML. Sans l'agent, le serveur plante dès le lancement (`ClassCastException` dans `Launch`). +- Le build NGServer de MCPC+ 1.6.4, compilé contre ASM 9 : même résultat, qu'il y ait ASM 9 ou l'ASM 4.1 d'origine dans `libraries/`. Sans l'agent, il plante au chargement du monde (`EnumHelper` : `sun.reflect.ReflectionFactory` absent, `java.lang` fermé). +- Un banc reproduisant launchwrapper 1.8 d'origine et l'`EnumHelper` de Forge 1.6.4, sous OpenJDK 17 et 21. ## Portée et limites -L'agent débloque le **lancement**. Il ne rend pas compatibles des mods qui dépendraient d'API supprimées en Java 8 — un cas rare sur cette génération de mods. +L'agent débloque le **lancement**. Il ne rend pas compatibles des mods ou plugins qui dépendraient d'API supprimées du JDK (moteur JavaScript Nashorn, `javax.xml.bind`, `Thread.stop`...) ; ces cas restent à traiter au cas par cas. Il ne touche à rien d'autre : pas de modification du monde, des mods, ni du comportement du serveur. Si un correctif ne peut pas s'appliquer, la classe d'origine est laissée intacte et le démarrage continue. -Testé sur MCPC+ 8.11 / Cauldron 1.6.4 avec OpenJDK 8 et Zulu 8, en production. +L'agent requiert Java 17 ou plus récent ; il ne se charge pas sous Java 8. ## Compiler @@ -65,10 +101,12 @@ Testé sur MCPC+ 8.11 / Cauldron 1.6.4 avec OpenJDK 8 et Zulu 8, en production. ./gradlew jar ``` -Le jar est produit dans `build/libs/mcpc-java8-agent.jar`. Aucune dépendance externe. +Nécessite un JDK 17 ou plus récent. La CI GitHub Actions construit le jar à chaque push ; pousser un tag `vX.Y.Z` publie une release avec le jar attaché. Le jar est produit dans `build/libs/mcpc-java17-agent.jar` et contient ASM (téléchargé depuis Maven Central et adapté au build, voir `build.gradle`) ; il n'a besoin de rien d'autre à l'exécution. ## Contenu -Les fichiers `.bin` sont les classes d'origine de **launchwrapper** et **ASM**, recompilées avec la correction minimale nécessaire. Elles restent sous la licence de leurs projets respectifs : MIT pour launchwrapper, BSD 3-clauses pour ASM. +`Launch.bin` est la classe d'origine de **launchwrapper** (MIT), recompilée avec la correction minimale nécessaire : son constructeur appelle `gg.nationsglory.mcpcj17.LaunchHooks`. + +Le jar embarque **ASM** (BSD 3-clauses) : ASM 9.7, les `Remapping*Adapter` d'ASM 7.0, et `org.objectweb.asm.LegacyAsmApi`, ajouté par l'agent. Le code de l'agent lui-même est publié sous licence MIT. diff --git a/build.gradle b/build.gradle index aa95ebc..662f352 100644 --- a/build.gradle +++ b/build.gradle @@ -1,26 +1,194 @@ +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.* + +buildscript { + repositories { mavenCentral() } + dependencies { classpath 'org.ow2.asm:asm-tree:9.7' } +} + plugins { id 'java' } group = 'gg.nationsglory' -version = '1.0.0' - -sourceCompatibility = '1.7' -targetCompatibility = '1.7' +version = '2.0.0' -compileJava { options.encoding = 'UTF-8' } +compileJava { + options.encoding = 'UTF-8' + options.release = 17 +} repositories { mavenCentral() } +def asmVersion = '9.7' + +configurations { + // ASM shipped inside the agent, replacing the ASM 4 of MCPC+ at runtime. + bundledAsm { transitive = false } + // The Remapping*Adapter classes Forge 1.6.4 still uses were removed after ASM 7.0. + legacyAsmCommons { transitive = false } +} + +dependencies { + bundledAsm "org.ow2.asm:asm:${asmVersion}" + bundledAsm "org.ow2.asm:asm-tree:${asmVersion}" + bundledAsm "org.ow2.asm:asm-commons:${asmVersion}" + bundledAsm "org.ow2.asm:asm-analysis:${asmVersion}" + bundledAsm "org.ow2.asm:asm-util:${asmVersion}" + legacyAsmCommons 'org.ow2.asm:asm-commons:7.0' +} + +/* + * The bundled ASM is patched to behave like the ASM 4 that Forge 1.6.4, MCPC+ and their mods were + * written for: + * - Type.getType(String) accepts an internal name ("net/minecraftforge/event/Event"), as ASM 4 + * did; Forge's EventTransformer relies on it. It returns the object type of that name. + * - Visitors declared with the ASM 4 API no longer throw "This feature requires ASM5" when they + * read Java 8+ bytecode (lambdas, interface static methods, type annotations...): the call is + * forwarded to the next visitor, as for any method the visitor does not override. + * - The interface flag of method calls survives ASM 4 visitors (see LegacyAsmApi). + */ +def patchedAsmDir = layout.buildDirectory.dir('patched-asm') +def legacyApi = 'org/objectweb/asm/LegacyAsmApi' +def asmPatches = [ + 'org/objectweb/asm/Type': { ClassNode node -> + def method = node.methods.find { it.name == 'getTypeInternal' } + def create = method.instructions.find { + it.opcode == Opcodes.NEW && it.desc == 'java/lang/IllegalArgumentException' + } + if (create == null) { + throw new GradleException('ASM Type.getTypeInternal: invalid descriptor branch not found') + } + def lenient = new InsnList() + lenient.add(new TypeInsnNode(Opcodes.NEW, 'org/objectweb/asm/Type')) + lenient.add(new InsnNode(Opcodes.DUP)) + lenient.add(new IntInsnNode(Opcodes.BIPUSH, 12)) // Type.INTERNAL + lenient.add(new VarInsnNode(Opcodes.ALOAD, 0)) + lenient.add(new VarInsnNode(Opcodes.ILOAD, 1)) + lenient.add(new VarInsnNode(Opcodes.ILOAD, 2)) + lenient.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, 'org/objectweb/asm/Type', '', + '(ILjava/lang/String;II)V', false)) + lenient.add(new InsnNode(Opcodes.ARETURN)) + replaceThrow(method, create, lenient) + }, + 'org/objectweb/asm/ClassVisitor': { ClassNode node -> removeApiChecks(node) }, + 'org/objectweb/asm/FieldVisitor': { ClassNode node -> removeApiChecks(node) }, + 'org/objectweb/asm/commons/InstructionAdapter': { ClassNode node -> removeApiChecks(node) }, + 'org/objectweb/asm/MethodVisitor': { ClassNode node -> + def fiveArgs = node.methods.find { + it.name == 'visitMethodInsn' && it.desc == '(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V' + } + def fourArgs = node.methods.find { + it.name == 'visitMethodInsn' && it.desc == '(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V' + } + // 5 args, ASM 4 visitor: record the interface flag instead of throwing, call the 4 args + // method, then drop the flag if nothing consumed it. + def interfaceCheck = fiveArgs.instructions.find { + it.opcode == Opcodes.LDC && it.cst instanceof String && it.cst.startsWith('INVOKESPECIAL/STATIC') + } + if (interfaceCheck == null) { + throw new GradleException('ASM MethodVisitor.visitMethodInsn: interface check not found') + } + def pend = new InsnList() + pend.add(new VarInsnNode(Opcodes.ILOAD, 5)) + pend.add(new MethodInsnNode(Opcodes.INVOKESTATIC, legacyApi, 'pendInterface', '(Z)V', false)) + replaceThrow(fiveArgs, previousNew(interfaceCheck), pend) + def legacyCall = fiveArgs.instructions.find { + it instanceof MethodInsnNode && it.name == 'visitMethodInsn' && it.desc == fourArgs.desc + } + fiveArgs.instructions.insert(legacyCall, + new MethodInsnNode(Opcodes.INVOKESTATIC, legacyApi, 'clearInterface', '()V', false)) + // 4 args: use the recorded flag rather than guessing it from the opcode. + def forward = fourArgs.instructions.find { + it instanceof MethodInsnNode && it.name == 'visitMethodInsn' && it.desc == fiveArgs.desc + } + fourArgs.instructions.insertBefore(forward, + new MethodInsnNode(Opcodes.INVOKESTATIC, legacyApi, 'interfaceFlag', '(Z)Z', false)) + removeApiChecks(node) + }, +] + +/** The NEW of the exception thrown right after {@code insn}. */ +def previousNew(AbstractInsnNode insn) { + while (insn.opcode != Opcodes.NEW) { + insn = insn.previous + } + return insn +} + +/** Replaces the instructions from {@code create} to the next ATHROW. */ +def replaceThrow(MethodNode method, AbstractInsnNode create, InsnList replacement) { + def insn = create + while (insn.opcode != Opcodes.ATHROW) { + def next = insn.next + method.instructions.remove(insn) + insn = next + } + method.instructions.insertBefore(insn, replacement) + method.instructions.remove(insn) +} + +/** Removes every {@code throw new UnsupportedOperationException(" requires ASMx")}. */ +def removeApiChecks(ClassNode node) { + int removed = 0 + node.methods.each { method -> + def checks = method.instructions.findAll { + it.opcode == Opcodes.LDC && it.cst instanceof String && it.cst ==~ /.* requires ASM\d+/ + } + checks.each { check -> + replaceThrow(method, previousNew(check), new InsnList()) + removed++ + } + } + if (removed == 0) { + throw new GradleException("ASM ${node.name}: no API check found") + } +} + +tasks.register('patchAsm') { + inputs.files configurations.bundledAsm + inputs.property 'patches', asmPatches.keySet().join(',') + outputs.dir patchedAsmDir + doLast { + asmPatches.each { name, patch -> + def bytes = configurations.bundledAsm.collect { jar -> + zipTree(jar).matching { include "${name}.class" }.files + }.flatten().first().bytes + def node = new ClassNode() + new ClassReader(bytes).accept(node, 0) + patch(node) + def writer = new ClassWriter(ClassWriter.COMPUTE_MAXS) + node.accept(writer) + def out = patchedAsmDir.get().file("${name}.class").asFile + out.parentFile.mkdirs() + out.bytes = writer.toByteArray() + } + } +} + jar { - archiveBaseName = 'mcpc-java8-agent' + dependsOn 'patchAsm' + from(patchedAsmDir) + archiveBaseName = 'mcpc-java17-agent' archiveVersion = '' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from({ configurations.bundledAsm.collect { zipTree(it) } }) { + include 'org/objectweb/asm/**' + exclude asmPatches.keySet().collect { "${it}.class" } + } + from({ configurations.legacyAsmCommons.collect { zipTree(it) } }) { + include 'org/objectweb/asm/commons/Remapping*Adapter*.class' + } manifest { attributes( - 'Premain-Class': 'gg.nationsglory.mcpcj8.McpcJava8Agent', + 'Premain-Class': 'gg.nationsglory.mcpcj17.McpcJava17Agent', + 'Can-Redefine-Classes': 'false', 'Can-Retransform-Classes': 'false', - 'Implementation-Title': 'MCPC+ Java 8 Agent', - 'Implementation-Version': project.version + 'Implementation-Title': 'MCPC+ Java 17 Agent', + 'Implementation-Version': project.version, + 'Bundled-ASM-Version': asmVersion ) } } diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index b826c81..2d022a4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'mcpc-java8-agent' +rootProject.name = 'mcpc-java17-agent' diff --git a/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java new file mode 100644 index 0000000..9eaf0d8 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/ClassPatcher.java @@ -0,0 +1,280 @@ +package gg.nationsglory.mcpcj17; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.ProtectionDomain; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public final class ClassPatcher implements ClassFileTransformer { + + private static final Map TARGETS = new HashMap(); + static { + TARGETS.put("net/minecraft/launchwrapper/Launch", "Launch.bin"); + } + + /** JDK 8 internals removed in Java 9, and their replacements in this agent. */ + private static final String[][] REDIRECTS = { + {"sun/reflect/ReflectionFactory", "gg/nationsglory/mcpcj17/shim/ReflectionFactory"}, + {"sun/reflect/ConstructorAccessor", "gg/nationsglory/mcpcj17/shim/ConstructorAccessor"}, + {"sun/reflect/FieldAccessor", "gg/nationsglory/mcpcj17/shim/FieldAccessor"}, + }; + private static final byte[] SUN_REFLECT_SLASH = "sun/reflect/".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SUN_REFLECT_DOT = "sun.reflect.".getBytes(StandardCharsets.US_ASCII); + + private static final String LAUNCH = "net/minecraft/launchwrapper/Launch"; + private static final String ASM_PREFIX = "org/objectweb/asm/"; + private static final String LAUNCH_CLASS_LOADER = "net.minecraft.launchwrapper.LaunchClassLoader"; + + private final Map patches = new HashMap(); + /** This agent's jar, which also carries a recent ASM. */ + private final JarFile agentJar; + private final Map legacyAsm = + Collections.synchronizedMap(new WeakHashMap()); + private final Map configuredLoaders = + Collections.synchronizedMap(new WeakHashMap()); + + public ClassPatcher() { + for (Map.Entry entry : TARGETS.entrySet()) { + byte[] bytes = readResource(entry.getValue()); + if (bytes != null) { + patches.put(entry.getKey(), bytes); + } else { + System.out.println("[mcpc-j17] Missing patch resource: " + entry.getValue()); + } + } + System.out.println("[mcpc-j17] " + patches.size() + " class patch(es) loaded."); + + JarFile jar = null; + try { + URL url = ClassPatcher.class.getProtectionDomain().getCodeSource().getLocation(); + jar = new JarFile(new File(url.toURI())); + if (jar.getEntry("org/objectweb/asm/ClassReader.class") == null) { + System.out.println("[mcpc-j17] No ASM bundled in the agent jar."); + jar.close(); + jar = null; + } + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not open the agent jar, ASM will not be upgraded: " + t); + } + this.agentJar = jar; + } + + @Override + public byte[] transform(ClassLoader loader, String className, Class redefined, + ProtectionDomain domain, byte[] original) { + try { + if (loader != null && LAUNCH_CLASS_LOADER.equals(loader.getClass().getName()) + && configuredLoaders.put(loader, Boolean.TRUE) == null) { + LaunchHooks.configure(loader, agentJar != null && isLegacyAsm(loader)); + } + if (className != null && className.startsWith(ASM_PREFIX)) { + byte[] upgraded = agentJar != null && isLegacyAsm(loader) ? bundledClass(className) : null; + if (upgraded != null) { + return upgraded; + } + } else if (className != null) { + byte[] patch = patches.get(className); + if (patch != null && needsPatch(loader, className, original)) { + System.out.println("[mcpc-j17] Patched: " + className); + return patch; + } + if (loader != null && !className.startsWith("gg/nationsglory/mcpcj17/") + && (indexOf(original, SUN_REFLECT_SLASH) >= 0 || indexOf(original, SUN_REFLECT_DOT) >= 0)) { + byte[] redirected = redirectSunReflect(original); + if (redirected != null) { + System.out.println("[mcpc-j17] Redirected sun.reflect in: " + className); + return redirected; + } + } + } + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not patch " + className + ", leaving it untouched: " + t); + } + return null; + } + + /** + * The bundled classes replace the stock launchwrapper 1.8 and ASM 4 ones. Servers that already + * ship a Java 9+ ready Launch, or a recent ASM, keep their own classes. + */ + private boolean needsPatch(ClassLoader loader, String className, byte[] original) { + if (className.equals(LAUNCH)) { + // The stock constructor casts the application class loader to URLClassLoader. + return indexOf(original, "java/net/URLClassLoader".getBytes(StandardCharsets.US_ASCII)) >= 0; + } + return true; + } + + /** The bundled ASM version of a class, or null for an ASM 4 class that no longer exists. */ + private byte[] bundledClass(String className) { + JarEntry entry = agentJar.getJarEntry(className + ".class"); + if (entry == null) { + return null; + } + try (InputStream in = agentJar.getInputStream(entry)) { + return in.readAllBytes(); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not read bundled " + className + ": " + t); + return null; + } + } + + /** + * MCPC+ ships ASM 4, which cannot read Java 8+ bytecode nor run coremods written for a recent + * ASM: it is then replaced by the ASM bundled in this agent. ASM 5 added type annotations; + * servers already on ASM 5 or later keep their own. + */ + private boolean isLegacyAsm(ClassLoader loader) { + ClassLoader key = loader != null ? loader : ClassLoader.getPlatformClassLoader(); + Boolean legacy = legacyAsm.get(key); + if (legacy == null) { + byte[] classReader = null; + try (InputStream in = key.getResourceAsStream("org/objectweb/asm/ClassReader.class")) { + if (in != null) { + classReader = in.readAllBytes(); + } + } catch (Throwable ignored) { + } + legacy = classReader != null + && indexOf(classReader, "readTypeAnnotations".getBytes(StandardCharsets.US_ASCII)) < 0; + if (legacy && agentJar != null) { + System.out.println("[mcpc-j17] ASM 4 detected, upgrading it to the bundled ASM."); + } else if (!legacy) { + System.out.println("[mcpc-j17] Recent ASM detected, keeping the server's ASM classes."); + } + legacyAsm.put(key, legacy); + } + return legacy; + } + + /** + * Rewrites the UTF-8 constants of the class that name one of the {@link #REDIRECTS}, in internal + * form (type references, descriptors) or dotted form (Class.forName strings). Returns null when + * nothing changed. + */ + static byte[] redirectSunReflect(byte[] b) { + int count = u2(b, 8); + ByteArrayOutputStream out = new ByteArrayOutputStream(b.length + 256); + out.write(b, 0, 10); + boolean changed = false; + int pos = 10; + for (int i = 1; i < count; i++) { + int tag = b[pos] & 0xFF; + int size; + switch (tag) { + case 1: + int length = u2(b, pos + 1); + String value = new String(b, pos + 3, length, StandardCharsets.ISO_8859_1); + String replaced = redirect(value); + if (replaced != value) { + byte[] bytes = replaced.getBytes(StandardCharsets.ISO_8859_1); + out.write(1); + out.write(bytes.length >>> 8); + out.write(bytes.length); + out.write(bytes, 0, bytes.length); + changed = true; + pos += 3 + length; + continue; + } + size = 3 + length; + break; + case 3: case 4: case 9: case 10: case 11: case 12: case 17: case 18: + size = 5; + break; + case 5: case 6: + size = 9; + i++; + break; + case 7: case 8: case 16: case 19: case 20: + size = 3; + break; + case 15: + size = 4; + break; + default: + throw new IllegalStateException("Unknown constant pool tag " + tag); + } + out.write(b, pos, size); + pos += size; + } + if (!changed) { + return null; + } + out.write(b, pos, b.length - pos); + return out.toByteArray(); + } + + /** Returns {@code value} itself when it holds no redirected name. */ + private static String redirect(String value) { + if (!value.contains("sun/reflect/") && !value.contains("sun.reflect.")) { + return value; + } + String result = value; + for (String[] redirect : REDIRECTS) { + result = replaceName(result, redirect[0], redirect[1]); + result = replaceName(result, redirect[0].replace('/', '.'), redirect[1].replace('/', '.')); + } + return result.equals(value) ? value : result; + } + + /** Replaces whole class names only: sun/reflect/FieldAccessor, not sun/reflect/FieldAccessorImpl. */ + private static String replaceName(String value, String from, String to) { + StringBuilder sb = null; + int copied = 0; + int search = 0; + int index; + while ((index = value.indexOf(from, search)) >= 0) { + int end = index + from.length(); + search = end; + if (end < value.length() && Character.isJavaIdentifierPart(value.charAt(end))) { + continue; + } + if (sb == null) { + sb = new StringBuilder(value.length() + 32); + } + sb.append(value, copied, index).append(to); + copied = end; + } + if (sb == null) { + return value; + } + return sb.append(value, copied, value.length()).toString(); + } + + private static int u2(byte[] b, int pos) { + return ((b[pos] & 0xFF) << 8) | (b[pos + 1] & 0xFF); + } + + private static int indexOf(byte[] data, byte[] pattern) { + outer: + for (int i = 0; i <= data.length - pattern.length; i++) { + for (int j = 0; j < pattern.length; j++) { + if (data[i + j] != pattern[j]) { + continue outer; + } + } + return i; + } + return -1; + } + + private static byte[] readResource(String name) { + try (InputStream in = ClassPatcher.class.getResourceAsStream(name)) { + if (in == null) { + return null; + } + return in.readAllBytes(); + } catch (Throwable t) { + return null; + } + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java b/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java new file mode 100644 index 0000000..7512f62 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/LaunchHooks.java @@ -0,0 +1,70 @@ +package gg.nationsglory.mcpcj17; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +/** + * Called by the patched {@code net.minecraft.launchwrapper.Launch} constructor. + */ +public final class LaunchHooks { + + /** + * launchwrapper builds its class loader from {@code ((URLClassLoader) getClassLoader()).getURLs()}; + * the application class loader is no longer a URLClassLoader since Java 9. + */ + public static URL[] classPath() { + List urls = new ArrayList(); + String classPath = System.getProperty("java.class.path", ""); + for (String entry : classPath.split(File.pathSeparator)) { + if (entry.isEmpty()) { + continue; + } + try { + urls.add(new File(entry).getAbsoluteFile().toURI().toURL()); + } catch (Exception e) { + System.out.println("[mcpc-j17] Ignoring class path entry " + entry + ": " + e); + } + } + return urls.toArray(new URL[0]); + } + + /** + * LaunchClassLoader has the bootstrap loader as parent. On Java 8 that covered the whole JDK; + * since Java 9, modules such as java.sql or java.scripting live in the platform loader, so their + * packages (javax.sql, javax.script...) must be delegated explicitly. + * + * @param shareAsm when ASM 4 is replaced by the ASM bundled in the agent: the loader then + * delegates ASM to the application class loader, which also finds the classes + * that did not exist in ASM 4 + */ + public static void configure(ClassLoader launchClassLoader, boolean shareAsm) { + try { + Method exclude = launchClassLoader.getClass().getMethod("addClassLoaderExclusion", String.class); + exclude.invoke(launchClassLoader, "gg.nationsglory.mcpcj17."); + if (shareAsm) { + exclude.invoke(launchClassLoader, "org.objectweb.asm."); + } + int count = 0; + for (Module module : ModuleLayer.boot().modules()) { + if (module.getClassLoader() == null) { + continue; + } + for (String pkg : module.getPackages()) { + if (!pkg.startsWith("java.")) { + exclude.invoke(launchClassLoader, pkg + "."); + count++; + } + } + } + System.out.println("[mcpc-j17] LaunchClassLoader delegates " + count + " platform packages to the JDK."); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not configure LaunchClassLoader, continuing: " + t); + } + } + + private LaunchHooks() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/McpcJava17Agent.java b/src/main/java/gg/nationsglory/mcpcj17/McpcJava17Agent.java new file mode 100644 index 0000000..eaba231 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/McpcJava17Agent.java @@ -0,0 +1,37 @@ +package gg.nationsglory.mcpcj17; + +import java.lang.instrument.Instrumentation; + +public final class McpcJava17Agent { + + private static final String SPOOFED_JAVA_VERSION = "1.7.0_80"; + + public static void premain(String agentArgs, Instrumentation inst) { + spoofJavaVersion(); + ModuleAccess.openJdkModules(inst); + ModuleAccess.removeReflectionFilters(); + installClassPatches(inst); + } + + private static void spoofJavaVersion() { + String actual = System.getProperty("java.version"); + if (actual != null && !actual.startsWith("1.7")) { + System.setProperty("java.version", SPOOFED_JAVA_VERSION); + System.out.println("[mcpc-j17] Java " + actual + " detected, reporting " + + SPOOFED_JAVA_VERSION + " to bypass the MCPC+ version check."); + } else { + System.out.println("[mcpc-j17] java.version = " + actual + ", nothing to bypass."); + } + } + + private static void installClassPatches(Instrumentation inst) { + try { + inst.addTransformer(new ClassPatcher()); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not install class patches, continuing: " + t); + } + } + + private McpcJava17Agent() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/ModuleAccess.java b/src/main/java/gg/nationsglory/mcpcj17/ModuleAccess.java new file mode 100644 index 0000000..a2f0063 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/ModuleAccess.java @@ -0,0 +1,71 @@ +package gg.nationsglory.mcpcj17; + +import java.lang.instrument.Instrumentation; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.lang.ref.SoftReference; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Gives the server the reflective access it had on Java 8. + */ +final class ModuleAccess { + + /** + * Equivalent of {@code --add-opens =ALL-UNNAMED}: Forge, Bukkit and mods + * call setAccessible on JDK internals, which Java 17 refuses by default. + */ + static void openJdkModules(Instrumentation inst) { + try { + Module self = ModuleAccess.class.getModule(); + inst.redefineModule(Object.class.getModule(), Set.of(), Map.of(), + Map.of("java.lang", Set.of(self)), Set.of(), Map.of()); + + Method openToAllUnnamed = Module.class.getDeclaredMethod("implAddOpensToAllUnnamed", String.class); + openToAllUnnamed.setAccessible(true); + int count = 0; + for (Module module : ModuleLayer.boot().modules()) { + for (String pkg : module.getPackages()) { + openToAllUnnamed.invoke(module, pkg); + count++; + } + } + System.out.println("[mcpc-j17] Opened " + count + " JDK packages to the server."); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not open JDK modules, continuing: " + t); + } + } + + /** + * Java 12+ hides some fields from reflection, notably {@code Field.modifiers}, which Forge's + * EnumHelper uses to write static final fields. Java 8 hid nothing of the kind. + */ + static void removeReflectionFilters() { + try { + Class reflection = Class.forName("jdk.internal.reflect.Reflection"); + VarHandle filterMap = MethodHandles.privateLookupIn(reflection, MethodHandles.lookup()) + .findStaticVarHandle(reflection, "fieldFilterMap", Map.class); + @SuppressWarnings("unchecked") + Map, ?> filtered = (Map, ?>) filterMap.getVolatile(); + filterMap.setVolatile(new HashMap, Set>()); + + // Drop the reflection caches that may already hold the filtered view. + if (filtered != null) { + VarHandle reflectionData = MethodHandles.privateLookupIn(Class.class, MethodHandles.lookup()) + .findVarHandle(Class.class, "reflectionData", SoftReference.class); + for (Class type : filtered.keySet()) { + reflectionData.setVolatile(type, (SoftReference) null); + } + } + System.out.println("[mcpc-j17] Reflection filters removed."); + } catch (Throwable t) { + System.out.println("[mcpc-j17] Could not remove reflection filters, continuing: " + t); + } + } + + private ModuleAccess() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/ConstructorAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/ConstructorAccessor.java new file mode 100644 index 0000000..d491345 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/ConstructorAccessor.java @@ -0,0 +1,12 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.reflect.InvocationTargetException; + +/** + * Stands in for {@code sun.reflect.ConstructorAccessor}. + */ +public interface ConstructorAccessor { + + Object newInstance(Object[] args) + throws InstantiationException, IllegalArgumentException, InvocationTargetException; +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/FieldAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/FieldAccessor.java new file mode 100644 index 0000000..bb565f0 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/FieldAccessor.java @@ -0,0 +1,43 @@ +package gg.nationsglory.mcpcj17.shim; + +/** + * Stands in for {@code sun.reflect.FieldAccessor}. + */ +public interface FieldAccessor { + + Object get(Object target) throws IllegalArgumentException; + + boolean getBoolean(Object target) throws IllegalArgumentException; + + byte getByte(Object target) throws IllegalArgumentException; + + char getChar(Object target) throws IllegalArgumentException; + + short getShort(Object target) throws IllegalArgumentException; + + int getInt(Object target) throws IllegalArgumentException; + + long getLong(Object target) throws IllegalArgumentException; + + float getFloat(Object target) throws IllegalArgumentException; + + double getDouble(Object target) throws IllegalArgumentException; + + void set(Object target, Object value) throws IllegalArgumentException, IllegalAccessException; + + void setBoolean(Object target, boolean value) throws IllegalArgumentException, IllegalAccessException; + + void setByte(Object target, byte value) throws IllegalArgumentException, IllegalAccessException; + + void setChar(Object target, char value) throws IllegalArgumentException, IllegalAccessException; + + void setShort(Object target, short value) throws IllegalArgumentException, IllegalAccessException; + + void setInt(Object target, int value) throws IllegalArgumentException, IllegalAccessException; + + void setLong(Object target, long value) throws IllegalArgumentException, IllegalAccessException; + + void setFloat(Object target, float value) throws IllegalArgumentException, IllegalAccessException; + + void setDouble(Object target, double value) throws IllegalArgumentException, IllegalAccessException; +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/MethodHandleConstructorAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/MethodHandleConstructorAccessor.java new file mode 100644 index 0000000..2698d42 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/MethodHandleConstructorAccessor.java @@ -0,0 +1,35 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Calls a constructor through a method handle, which, unlike {@code Constructor.newInstance}, + * also works for enum constructors. + */ +final class MethodHandleConstructorAccessor implements ConstructorAccessor { + + private final MethodHandle handle; + + MethodHandleConstructorAccessor(Constructor constructor) { + try { + handle = MethodHandles.privateLookupIn(constructor.getDeclaringClass(), MethodHandles.lookup()) + .unreflectConstructor(constructor); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot access " + constructor, e); + } + } + + @Override + public Object newInstance(Object[] args) throws InvocationTargetException { + try { + return handle.invokeWithArguments(args == null ? new Object[0] : args); + } catch (ClassCastException | IllegalArgumentException | NullPointerException e) { + throw new IllegalArgumentException(e); + } catch (Throwable t) { + throw new InvocationTargetException(t); + } + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/ReflectionFactory.java b/src/main/java/gg/nationsglory/mcpcj17/shim/ReflectionFactory.java new file mode 100644 index 0000000..eceb85f --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/ReflectionFactory.java @@ -0,0 +1,29 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +/** + * Stands in for {@code sun.reflect.ReflectionFactory}, removed in Java 9. Forge's EnumHelper (and + * similar code in MCPC+ and mods) uses it to create enum constants and write static final fields; + * references to it are redirected here by {@link gg.nationsglory.mcpcj17.ClassPatcher}. + */ +public final class ReflectionFactory { + + private static final ReflectionFactory INSTANCE = new ReflectionFactory(); + + public static ReflectionFactory getReflectionFactory() { + return INSTANCE; + } + + public ConstructorAccessor newConstructorAccessor(Constructor constructor) { + return new MethodHandleConstructorAccessor(constructor); + } + + public FieldAccessor newFieldAccessor(Field field, boolean override) { + return new UnsafeFieldAccessor(field); + } + + private ReflectionFactory() { + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj17/shim/UnsafeFieldAccessor.java b/src/main/java/gg/nationsglory/mcpcj17/shim/UnsafeFieldAccessor.java new file mode 100644 index 0000000..ba9e887 --- /dev/null +++ b/src/main/java/gg/nationsglory/mcpcj17/shim/UnsafeFieldAccessor.java @@ -0,0 +1,194 @@ +package gg.nationsglory.mcpcj17.shim; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import sun.misc.Unsafe; + +/** + * Reads and writes a field through {@link Unsafe}, including static final fields, as + * {@code sun.reflect.FieldAccessor} allowed on Java 8. + */ +@SuppressWarnings({"deprecation", "removal"}) +final class UnsafeFieldAccessor implements FieldAccessor { + + private static final Unsafe UNSAFE; + static { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + UNSAFE = (Unsafe) theUnsafe.get(null); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private final Field field; + private final boolean isStatic; + private final Object staticBase; + private final long offset; + + UnsafeFieldAccessor(Field field) { + this.field = field; + this.isStatic = Modifier.isStatic(field.getModifiers()); + if (isStatic) { + this.staticBase = UNSAFE.staticFieldBase(field); + this.offset = UNSAFE.staticFieldOffset(field); + } else { + this.staticBase = null; + this.offset = UNSAFE.objectFieldOffset(field); + } + } + + private Object base(Object target) { + if (isStatic) { + return staticBase; + } + if (!field.getDeclaringClass().isInstance(target)) { + throw new IllegalArgumentException("Cannot access field " + field + " on " + target); + } + return target; + } + + private Class type() { + return field.getType(); + } + + @Override + public Object get(Object target) { + Object base = base(target); + Class type = type(); + if (!type.isPrimitive()) { + return UNSAFE.getObjectVolatile(base, offset); + } else if (type == boolean.class) { + return UNSAFE.getBooleanVolatile(base, offset); + } else if (type == byte.class) { + return UNSAFE.getByteVolatile(base, offset); + } else if (type == char.class) { + return UNSAFE.getCharVolatile(base, offset); + } else if (type == short.class) { + return UNSAFE.getShortVolatile(base, offset); + } else if (type == int.class) { + return UNSAFE.getIntVolatile(base, offset); + } else if (type == long.class) { + return UNSAFE.getLongVolatile(base, offset); + } else if (type == float.class) { + return UNSAFE.getFloatVolatile(base, offset); + } else { + return UNSAFE.getDoubleVolatile(base, offset); + } + } + + @Override + public void set(Object target, Object value) { + Object base = base(target); + Class type = type(); + if (!type.isPrimitive()) { + if (value != null && !type.isInstance(value)) { + throw new IllegalArgumentException("Cannot set " + field + " to " + value.getClass().getName()); + } + UNSAFE.putObjectVolatile(base, offset, value); + } else if (value == null) { + throw new IllegalArgumentException("Cannot set primitive field " + field + " to null"); + } else if (type == boolean.class) { + UNSAFE.putBooleanVolatile(base, offset, (Boolean) value); + } else if (type == byte.class) { + UNSAFE.putByteVolatile(base, offset, (Byte) value); + } else if (type == char.class) { + UNSAFE.putCharVolatile(base, offset, (Character) value); + } else if (type == short.class) { + UNSAFE.putShortVolatile(base, offset, ((Number) value).shortValue()); + } else if (type == int.class) { + UNSAFE.putIntVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).intValue()); + } else if (type == long.class) { + UNSAFE.putLongVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).longValue()); + } else if (type == float.class) { + UNSAFE.putFloatVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).floatValue()); + } else { + UNSAFE.putDoubleVolatile(base, offset, value instanceof Character ? (Character) value : ((Number) value).doubleValue()); + } + } + + @Override + public boolean getBoolean(Object target) { + return (Boolean) get(target); + } + + @Override + public byte getByte(Object target) { + return ((Number) get(target)).byteValue(); + } + + @Override + public char getChar(Object target) { + return (Character) get(target); + } + + @Override + public short getShort(Object target) { + return ((Number) get(target)).shortValue(); + } + + @Override + public int getInt(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).intValue(); + } + + @Override + public long getLong(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).longValue(); + } + + @Override + public float getFloat(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).floatValue(); + } + + @Override + public double getDouble(Object target) { + Object value = get(target); + return value instanceof Character ? (Character) value : ((Number) value).doubleValue(); + } + + @Override + public void setBoolean(Object target, boolean value) { + set(target, value); + } + + @Override + public void setByte(Object target, byte value) { + set(target, value); + } + + @Override + public void setChar(Object target, char value) { + set(target, value); + } + + @Override + public void setShort(Object target, short value) { + set(target, value); + } + + @Override + public void setInt(Object target, int value) { + set(target, value); + } + + @Override + public void setLong(Object target, long value) { + set(target, value); + } + + @Override + public void setFloat(Object target, float value) { + set(target, value); + } + + @Override + public void setDouble(Object target, double value) { + set(target, value); + } +} diff --git a/src/main/java/gg/nationsglory/mcpcj8/Java8ClassPatcher.java b/src/main/java/gg/nationsglory/mcpcj8/Java8ClassPatcher.java deleted file mode 100644 index d18c46c..0000000 --- a/src/main/java/gg/nationsglory/mcpcj8/Java8ClassPatcher.java +++ /dev/null @@ -1,77 +0,0 @@ -package gg.nationsglory.mcpcj8; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.lang.instrument.ClassFileTransformer; -import java.security.ProtectionDomain; -import java.util.HashMap; -import java.util.Map; - -public final class Java8ClassPatcher implements ClassFileTransformer { - - private static final Map TARGETS = new HashMap(); - static { - TARGETS.put("net/minecraft/launchwrapper/Launch", "Launch.bin"); - TARGETS.put("org/objectweb/asm/commons/Remapper", "Remapper.bin"); - TARGETS.put("org/objectweb/asm/ClassReader", "ClassReader.bin"); - TARGETS.put("org/objectweb/asm/tree/MethodInsnNode", "MethodInsnNode.bin"); - } - - private final Map patches = new HashMap(); - - public Java8ClassPatcher() { - for (Map.Entry entry : TARGETS.entrySet()) { - byte[] bytes = readResource(entry.getValue()); - if (bytes != null) { - patches.put(entry.getKey(), bytes); - } else { - System.out.println("[mcpc-j8] Missing patch resource: " + entry.getValue()); - } - } - System.out.println("[mcpc-j8] " + patches.size() + " class patch(es) loaded."); - } - - public boolean hasPatches() { - return !patches.isEmpty(); - } - - @Override - public byte[] transform(ClassLoader loader, String className, Class redefined, - ProtectionDomain domain, byte[] original) { - try { - if (className != null) { - byte[] patch = patches.get(className); - if (patch != null) { - System.out.println("[mcpc-j8] Patched: " + className); - return patch; - } - } - } catch (Throwable t) { - return null; - } - return null; - } - - private static byte[] readResource(String name) { - InputStream in = Java8ClassPatcher.class.getResourceAsStream(name); - if (in == null) { - return null; - } - try { - ByteArrayOutputStream out = new ByteArrayOutputStream(8192); - byte[] buffer = new byte[8192]; - int read; - while ((read = in.read(buffer)) > 0) { - out.write(buffer, 0, read); - } - return out.toByteArray(); - } catch (Throwable t) { - return null; - } finally { - try { - in.close(); - } catch (Throwable ignored) { - } - } - } -} diff --git a/src/main/java/gg/nationsglory/mcpcj8/McpcJava8Agent.java b/src/main/java/gg/nationsglory/mcpcj8/McpcJava8Agent.java deleted file mode 100644 index 55f7c5c..0000000 --- a/src/main/java/gg/nationsglory/mcpcj8/McpcJava8Agent.java +++ /dev/null @@ -1,48 +0,0 @@ -package gg.nationsglory.mcpcj8; - -import java.lang.instrument.Instrumentation; - -public final class McpcJava8Agent { - - private static final String SPOOFED_JAVA_VERSION = "1.7.0_80"; - - public static void premain(String agentArgs, Instrumentation inst) { - spoofJavaVersion(); - installClassPatches(inst); - } - - public static void premain(String agentArgs) { - spoofJavaVersion(); - System.out.println("[mcpc-j8] No Instrumentation available: only the version check is bypassed."); - } - - private static void spoofJavaVersion() { - String actual = System.getProperty("java.version"); - if (actual != null && actual.startsWith("1.8")) { - System.setProperty("java.version", SPOOFED_JAVA_VERSION); - System.out.println("[mcpc-j8] Java 8 detected (" + actual + "), reporting " - + SPOOFED_JAVA_VERSION + " to bypass the MCPC+ version check."); - } else { - System.out.println("[mcpc-j8] java.version = " + actual + ", nothing to bypass."); - } - } - - private static void installClassPatches(Instrumentation inst) { - if (inst == null) { - return; - } - try { - Java8ClassPatcher patcher = new Java8ClassPatcher(); - if (patcher.hasPatches()) { - inst.addTransformer(patcher); - } else { - System.out.println("[mcpc-j8] No class patches bundled: only the version check is bypassed."); - } - } catch (Throwable t) { - System.out.println("[mcpc-j8] Could not install class patches, continuing: " + t); - } - } - - private McpcJava8Agent() { - } -} diff --git a/src/main/java/org/objectweb/asm/LegacyAsmApi.java b/src/main/java/org/objectweb/asm/LegacyAsmApi.java new file mode 100644 index 0000000..8ee0aa9 --- /dev/null +++ b/src/main/java/org/objectweb/asm/LegacyAsmApi.java @@ -0,0 +1,36 @@ +package org.objectweb.asm; + +/** + * Support for visitors written against the ASM 4 API, patched into the bundled ASM at build time. + * + *

An ASM 4 visitor only has {@code visitMethodInsn(opcode, owner, name, descriptor)}: ASM 9 + * cannot pass it whether the owner is an interface, and refuses the call when that flag cannot be + * guessed from the opcode (invokestatic of a Java 8 static interface method). The patched + * {@link MethodVisitor} records the flag here before calling the 4 argument method, and restores it + * when that method forwards the instruction to the next visitor. + */ +public final class LegacyAsmApi { + + private static final ThreadLocal PENDING_INTERFACE = new ThreadLocal(); + + public static void pendInterface(boolean isInterface) { + PENDING_INTERFACE.set(isInterface); + } + + public static void clearInterface() { + PENDING_INTERFACE.remove(); + } + + /** The recorded flag if any, else the one guessed from the opcode. */ + public static boolean interfaceFlag(boolean guessed) { + Boolean pending = PENDING_INTERFACE.get(); + if (pending == null) { + return guessed; + } + PENDING_INTERFACE.remove(); + return pending; + } + + private LegacyAsmApi() { + } +} diff --git a/src/main/resources/gg/nationsglory/mcpcj17/Launch.bin b/src/main/resources/gg/nationsglory/mcpcj17/Launch.bin new file mode 100644 index 0000000..27ed326 Binary files /dev/null and b/src/main/resources/gg/nationsglory/mcpcj17/Launch.bin differ diff --git a/src/main/resources/gg/nationsglory/mcpcj8/ClassReader.bin b/src/main/resources/gg/nationsglory/mcpcj8/ClassReader.bin deleted file mode 100644 index d536fea..0000000 Binary files a/src/main/resources/gg/nationsglory/mcpcj8/ClassReader.bin and /dev/null differ diff --git a/src/main/resources/gg/nationsglory/mcpcj8/Launch.bin b/src/main/resources/gg/nationsglory/mcpcj8/Launch.bin deleted file mode 100644 index 86a5e3d..0000000 Binary files a/src/main/resources/gg/nationsglory/mcpcj8/Launch.bin and /dev/null differ diff --git a/src/main/resources/gg/nationsglory/mcpcj8/MethodInsnNode.bin b/src/main/resources/gg/nationsglory/mcpcj8/MethodInsnNode.bin deleted file mode 100644 index 4ef3233..0000000 Binary files a/src/main/resources/gg/nationsglory/mcpcj8/MethodInsnNode.bin and /dev/null differ diff --git a/src/main/resources/gg/nationsglory/mcpcj8/Remapper.bin b/src/main/resources/gg/nationsglory/mcpcj8/Remapper.bin deleted file mode 100644 index fc97a80..0000000 Binary files a/src/main/resources/gg/nationsglory/mcpcj8/Remapper.bin and /dev/null differ