import org.gradle.api.GradleException; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class ModdedArtifactVerifierTest { private static final String METADATA = "fabric.mod.json"; private static final String CODEC_CLASS = "art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class"; private static final String HARDWARE_CLASS = "art/arcane/iris/platform/bootstrap/getHardware.class"; private static final String MIXIN_CONFIG = "volmlib.entity.mixins.json"; private static final String CLIENT_MIXIN_CONFIG = "volmlib.client.mixins.json"; private static final String NEOFORGE_METADATA = "META-INF/neoforge.mods.toml"; private static final List REQUIRED_ENTRIES = List.of(METADATA, CODEC_CLASS, HARDWARE_CLASS); private static final byte[] INTERNAL_CODEC = ( "net/jpountz/lz4/LZ4BlockInputStream net/jpountz/lz4/LZ4BlockOutputStream" ).getBytes(StandardCharsets.ISO_8859_1); @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void rejectsClassesNewerThanSelectedRuntimeIncludingNestedJars() throws Exception { ClassWriter writer = new ClassWriter(0); writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, "example/NewRuntime", null, "java/lang/Object", null); writer.visitEnd(); Map entries = new LinkedHashMap<>(); entries.put("example/NewRuntime.class", writer.toByteArray()); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verifyJavaRuntime(artifact, 21)); assertTrue(failure.getMessage().contains("requires Java 25")); ModdedArtifactVerifier.verifyJavaRuntime(artifact, 25); File nested = createArtifact(Map.of("META-INF/jars/library.jar", Files.readAllBytes(artifact.toPath()))); GradleException nestedFailure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verifyJavaRuntime(nested, 21)); assertTrue(nestedFailure.getMessage().contains("library.jar!/example/NewRuntime.class")); } @Test public void acceptsMinecraftRuntimeReferences() throws Exception { Map entries = validEntries(); File artifact = createArtifact(entries); ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); } @Test public void rejectsBukkitDependencyBootstrapClassesAndNestedClasses() throws Exception { for (String internalName : List.of("art/arcane/iris/platform/bootstrap/SlimJar", "art/arcane/iris/platform/bootstrap/SlimJar$BootstrapLogger", "art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries", "art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries$Provider")) { Map entries = validEntries(); entries.put(internalName + ".class", classExtending(internalName, "java/lang/Object")); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("Bukkit dependency bootstrap code")); assertTrue(failure.getMessage().contains(internalName)); } } @Test public void rejectsDanglingBukkitDependencyBootstrapReferences() throws Exception { for (String internalName : List.of("art/arcane/iris/platform/bootstrap/SlimJar", "art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries")) { Map entries = validEntries(); String caller = "art/arcane/iris/modded/BootstrapCaller"; ClassWriter writer = new ClassWriter(0); writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, caller, null, "java/lang/Object", null); MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "dependencyType", "()Ljava/lang/Class;", null, null); method.visitCode(); method.visitLdcInsn(Type.getObjectType(internalName)); method.visitInsn(Opcodes.ARETURN); method.visitMaxs(1, 0); method.visitEnd(); writer.visitEnd(); entries.put(caller + ".class", writer.toByteArray()); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains(caller)); assertTrue(failure.getMessage().contains(internalName)); } } @Test public void rejectsBukkitDependencyBootstrapInsideJarJar() throws Exception { Map entries = validEntries(); String internalName = "art/arcane/iris/platform/bootstrap/SlimJar"; entries.put("META-INF/jarjar/dependency-support.jar", createNestedArtifact(Map.of( internalName + ".class", classExtending(internalName, "java/lang/Object")))); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("Bukkit dependency bootstrap code")); assertTrue(failure.getMessage().contains("dependency-support.jar!/" + internalName)); } @Test public void rejectsExecutableEntriesRegardlessOfCase() throws Exception { for (String executable : List.of("bin/installer.exe", "native/windows/helper.EXE")) { Map entries = validEntries(); entries.put(executable, new byte[]{1}); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("executable .exe entries")); assertTrue(failure.getMessage().contains(executable)); } } @Test public void rejectsExecutableInsideJarJar() throws Exception { Map entries = validEntries(); entries.put("META-INF/jarjar/native-support.jar", createNestedArtifact(Map.of( "native/helper.exe", new byte[]{1}))); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("executable .exe entries")); assertTrue(failure.getMessage().contains("native-support.jar!/native/helper.exe")); } @Test public void acceptsBundledCompressionLibraries() throws Exception { Map entries = validEntries(); entries.put("native/windows-x86_64/jlibdeflate.dll", new byte[]{1}); entries.put("native/linux-x86_64/libjlibdeflate.so", new byte[]{1}); entries.put("native/osx-aarch64/libjlibdeflate.dylib", new byte[]{1}); entries.put("com/fulcrumgenomics/jlibdeflate/NativeLoader.class", classExtending("com/fulcrumgenomics/jlibdeflate/NativeLoader", "java/lang/Object")); ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES); } @Test public void rejectsClientLanguagesWithoutPackMetadata() throws Exception { Map entries = validEntries(); entries.put("assets/irisworldgen/lang/en_us.json", "{}".getBytes(StandardCharsets.UTF_8)); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("client language resources without pack.mcmeta")); } @Test public void acceptsClientLanguagesWithPackMetadata() throws Exception { Map entries = validEntries(); entries.put("assets/irisworldgen/lang/en_us.json", "{}".getBytes(StandardCharsets.UTF_8)); entries.put("pack.mcmeta", ("{\"pack\":{\"description\":\"Iris\"," + "\"min_format\":[88,0],\"max_format\":[107,1]}}") .getBytes(StandardCharsets.UTF_8)); ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES); } @Test public void rejectsRelocatedRuntimeReference() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/Test.class", "art/arcane/iris/shadow/jpountz/lz4/LZ4BlockInputStream" .getBytes(StandardCharsets.ISO_8859_1)); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("Iris-private runtime references")); } @Test public void rejectsBukkitRelocatedGsonReferences() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/Test.class", "art/arcane/iris/util/gson/Gson" .getBytes(StandardCharsets.ISO_8859_1)); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("Iris-private runtime references")); } @Test public void rejectsBundledMinecraftRuntimeLibrary() throws Exception { Map entries = validEntries(); entries.put("net/jpountz/lz4/LZ4BlockInputStream.class", new byte[]{0}); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries")); } @Test public void rejectsNestedMinecraftRuntimeLibrary() throws Exception { Map entries = validEntries(); entries.put("META-INF/jars/lz4-java.jar", new byte[]{0}); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries")); } @Test public void rejectsRenamedJarJarRuntimeLibrary() throws Exception { Map entries = validEntries(); entries.put("META-INF/jarjar/runtime-support.jar", createNestedArtifact(Map.of( "com/sun/jna/Native.class", new byte[]{0} ))); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries")); } @Test public void rejectsArtifactWithoutPlatformMetadata() throws Exception { Map entries = new LinkedHashMap<>(); entries.put(CODEC_CLASS, INTERNAL_CODEC); entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("is missing " + METADATA)); } @Test public void rejectsUnrelocatedAsm() throws Exception { Map entries = validEntries(); entries.put("org/objectweb/asm/ClassReader.class", new byte[]{0}); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("ships unrelocated ASM")); } @Test public void rejectsNestedJarMissingFromMetadata() throws Exception { Map entries = validEntries(); entries.put("META-INF/jars/fabric-rendering-v1.jar", createNestedArtifact(Map.of( "net/fabricmc/Placeholder.class", new byte[]{0} ))); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("does not declare")); } @Test public void rejectsDeclaredNestedJarThatIsNotShipped() throws Exception { Map entries = validEntries(); entries.put(METADATA, fabricMetadata( "[ { \"file\": \"META-INF/jars/fabric-api-base.jar\" } ]", "[ \"" + MIXIN_CONFIG + "\" ]")); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("does not ship")); } @Test public void acceptsNestedJarDeclaredInMetadata() throws Exception { Map entries = validEntries(); entries.put(METADATA, fabricMetadata( "[ { \"file\": \"META-INF/jars/fabric-api-base.jar\" } ]", "[ \"" + MIXIN_CONFIG + "\" ]")); entries.put("META-INF/jars/fabric-api-base.jar", createNestedArtifact(Map.of( "net/fabricmc/Placeholder.class", new byte[]{0} ))); File artifact = createArtifact(entries); ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); } @Test public void rejectsMixinCompatibilityLevelAboveJava21() throws Exception { Map entries = validEntries(); entries.put(MIXIN_CONFIG, mixinConfig("JAVA_25")); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25")); } @Test public void rejectsMixinCompatibilityLevelAboveJava21RegisteredByManifest() throws Exception { Map entries = new LinkedHashMap<>(); entries.put("META-INF/mods.toml", "modId = \"irisworldgen\"".getBytes(StandardCharsets.UTF_8)); entries.put(CODEC_CLASS, INTERNAL_CODEC); entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); entries.put(CLIENT_MIXIN_CONFIG, mixinConfig("JAVA_25")); File artifact = createArtifact(entries, manifestWithMixinConfigs(CLIENT_MIXIN_CONFIG)); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, List.of(CODEC_CLASS, HARDWARE_CLASS))); assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25")); } @Test public void rejectsMixinCompatibilityLevelAboveJava21RegisteredByNeoforgeToml() throws Exception { Map entries = new LinkedHashMap<>(); entries.put(NEOFORGE_METADATA, ("[[mixins]]\nconfig = \"" + CLIENT_MIXIN_CONFIG + "\"\n") .getBytes(StandardCharsets.UTF_8)); entries.put(CODEC_CLASS, INTERNAL_CODEC); entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); entries.put(CLIENT_MIXIN_CONFIG, mixinConfig("JAVA_25")); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, List.of(CODEC_CLASS, HARDWARE_CLASS))); assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25")); } @Test public void rejectsRegisteredMixinConfigThatIsNotShipped() throws Exception { Map entries = validEntries(); entries.remove(MIXIN_CONFIG); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("which is not in the jar")); } @Test public void rejectsArtifactWithoutMixinRegistration() throws Exception { Map entries = validEntries(); entries.put(METADATA, fabricMetadata("[ ]", "[ ]")); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("registers no mixin configs")); } @Test public void rejectsBukkitSupertypeOutsidePlatformPackage() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/platform/generation/BukkitChunkGenerator.class", classExtending("art/arcane/iris/platform/generation/BukkitChunkGenerator", "org/bukkit/generator/ChunkGenerator")); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("org.bukkit supertype")); } @Test public void rejectsBukkitInterfaceOutsidePlatformPackage() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/Listener.class", classImplementing("art/arcane/iris/Listener", "org/bukkit/event/Listener")); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES)); assertTrue(failure.getMessage().contains("org.bukkit supertype")); } @Test public void acceptsBukkitSupertypeInsidePlatformPackage() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/platform/bukkit/BukkitWorld.class", classImplementing("art/arcane/iris/platform/bukkit/BukkitWorld", "org/bukkit/event/Listener")); File artifact = createArtifact(entries); ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); } @Test public void acceptsBukkitSupertypeDeclaredInThePurityAllowlist() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/platform/generation/BukkitChunkGenerator.class", classExtending("art/arcane/iris/platform/generation/BukkitChunkGenerator", "org/bukkit/generator/ChunkGenerator")); File artifact = createArtifact(entries); ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, Set.of("art/arcane/iris/platform/generation/BukkitChunkGenerator")); } @Test public void acceptsBukkitSupertypeOnNestedClassOfAllowlistedSource() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/world/entity/IrisEntity$BukkitOps$1.class", classImplementing("art/arcane/iris/world/entity/IrisEntity$BukkitOps$1", "org/bukkit/event/Listener")); File artifact = createArtifact(entries); ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, Set.of("art/arcane/iris/world/entity/IrisEntity")); } @Test public void acceptsBukkitSupertypeFromSharedLibrary() throws Exception { Map entries = validEntries(); entries.put("art/arcane/volmlib/util/bukkit/Events.class", classImplementing("art/arcane/volmlib/util/bukkit/Events", "org/bukkit/event/Listener")); File artifact = createArtifact(entries); ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES); } @Test public void rejectsBukkitSupertypeMissingFromThePurityAllowlist() throws Exception { Map entries = validEntries(); entries.put("art/arcane/iris/integration/data/OraxenDataProvider.class", classImplementing("art/arcane/iris/integration/data/OraxenDataProvider", "org/bukkit/event/Listener")); File artifact = createArtifact(entries); GradleException failure = assertThrows(GradleException.class, () -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, Set.of("art/arcane/iris/integration/data/ItemAdderDataProvider"))); assertTrue(failure.getMessage().contains("core/purity-allowlist.txt")); } private Map validEntries() { Map entries = new LinkedHashMap<>(); entries.put(METADATA, fabricMetadata("[ ]", "[ \"" + MIXIN_CONFIG + "\" ]")); entries.put(MIXIN_CONFIG, mixinConfig("JAVA_21")); entries.put(CODEC_CLASS, INTERNAL_CODEC); entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1)); return entries; } private byte[] fabricMetadata(String jarsArray, String mixinsArray) { return ("{\n" + " \"schemaVersion\": 1,\n" + " \"id\": \"irisworldgen\",\n" + " \"mixins\": " + mixinsArray + ",\n" + " \"jars\": " + jarsArray + "\n" + "}\n").getBytes(StandardCharsets.UTF_8); } private byte[] mixinConfig(String compatibilityLevel) { return ("{\n" + " \"required\": true,\n" + " \"minVersion\": \"0.8\",\n" + " \"package\": \"art.arcane.iris.modded.mixin\",\n" + " \"compatibilityLevel\": \"" + compatibilityLevel + "\"\n" + "}\n").getBytes(StandardCharsets.UTF_8); } private Manifest manifestWithMixinConfigs(String configs) { Manifest manifest = new Manifest(); manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); manifest.getMainAttributes().putValue("MixinConfigs", configs); return manifest; } private byte[] classExtending(String internalName, String superName) { ClassWriter writer = new ClassWriter(0); writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, superName, null); writer.visitEnd(); return writer.toByteArray(); } private byte[] classImplementing(String internalName, String interfaceName) { ClassWriter writer = new ClassWriter(0); writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", new String[]{interfaceName}); writer.visitEnd(); return writer.toByteArray(); } private File createArtifact(Map entries) throws Exception { return createArtifact(entries, null); } private File createArtifact(Map entries, Manifest manifest) throws Exception { File artifact = temporaryFolder.newFile("artifact-" + System.nanoTime() + ".jar"); try (FileOutputStream file = new FileOutputStream(artifact); JarOutputStream output = manifest == null ? new JarOutputStream(file) : new JarOutputStream(file, manifest)) { for (Map.Entry entry : entries.entrySet()) { output.putNextEntry(new JarEntry(entry.getKey())); output.write(entry.getValue()); output.closeEntry(); } } return artifact; } private byte[] createNestedArtifact(Map entries) throws Exception { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try (JarOutputStream output = new JarOutputStream(bytes)) { for (Map.Entry entry : entries.entrySet()) { output.putNextEntry(new JarEntry(entry.getKey())); output.write(entry.getValue()); output.closeEntry(); } } return bytes.toByteArray(); } }