This commit is contained in:
Brian Neumann-Fopiano
2026-08-01 22:39:09 -04:00
parent 324cf9e095
commit a8217b42e9
220 changed files with 10626 additions and 1661 deletions
@@ -1,4 +1,5 @@
import org.gradle.api.GradleException;
import org.objectweb.asm.ClassReader;
import java.io.File;
import java.io.IOException;
@@ -12,6 +13,9 @@ import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class ModdedArtifactVerifier {
private static final String CODEC_CLASS = "art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class";
@@ -33,11 +37,40 @@ public final class ModdedArtifactVerifier {
"art/arcane/iris/shadow/oshi",
"art/arcane/iris/shadow/jna"
);
// ASM must always be relocated: the loaders put their own copy on the module layer and a second
// org.objectweb.asm package on the same layer is a boot failure.
private static final String ASM_PREFIX = "org/objectweb/asm/";
// Only the Bukkit platform binding is allowed to sit on org.bukkit types, and the modded jars
// exclude it outright. Kept as an exemption so the same check can run over the Bukkit artifact.
private static final String BUKKIT_PLATFORM_PREFIX = "art/arcane/iris/platform/bukkit/";
private static final String BUKKIT_TYPE_PREFIX = "org/bukkit/";
private static final String FABRIC_METADATA = "fabric.mod.json";
private static final String NEOFORGE_METADATA = "META-INF/neoforge.mods.toml";
private static final String NESTED_JAR_DIRECTORY = "META-INF/jars/";
private static final String MIXIN_CONFIGS_ATTRIBUTE = "MixinConfigs";
// Forge 26.2 ships upstream Mixin 0.8.7, whose MixinEnvironment.CompatibilityLevel enum ends at
// JAVA_21. Anything higher is a hard MixinInitialisationError at Forge boot even though Fabric's
// fork accepts it, so the shared configs are capped here.
private static final int MAX_MIXIN_COMPATIBILITY_LEVEL = 21;
private static final Pattern TOML_CONFIG_ASSIGNMENT = Pattern.compile("(?m)^\\s*config\\s*=\\s*\"([^\"]+)\"");
private static final Pattern COMPATIBILITY_LEVEL = Pattern.compile("\"compatibilityLevel\"\\s*:\\s*\"([^\"]*)\"");
private static final Pattern COMPATIBILITY_LEVEL_VALUE = Pattern.compile("JAVA_(\\d+)");
private static final Pattern QUOTED_JSON_FILE = Pattern.compile("\"([^\"]+\\.(?:json|jar))\"");
private ModdedArtifactVerifier() {
}
public static void verify(File artifact, List<String> requiredEntries) {
verify(artifact, requiredEntries, Set.of());
}
/**
* @param bukkitSupertypeBaseline entry names (as they appear in the jar) that are known to
* extend or implement an {@code org.bukkit} type. New offenders
* outside this frozen set fail the build; entries that disappear
* from the artifact are not an error.
*/
public static void verify(File artifact, List<String> requiredEntries, Set<String> bukkitSupertypeBaseline) {
if (!artifact.isFile()) {
throw new GradleException("Missing modded Iris artifact: " + artifact.getAbsolutePath());
}
@@ -52,6 +85,9 @@ public final class ModdedArtifactVerifier {
Set<String> bundledRuntimeEntries = new LinkedHashSet<>();
Set<String> privateReferenceClasses = new LinkedHashSet<>();
Set<String> codecReferences = new LinkedHashSet<>();
Set<String> asmEntries = new LinkedHashSet<>();
Set<String> shippedNestedJars = new LinkedHashSet<>();
Set<String> bukkitSupertypeClasses = new LinkedHashSet<>();
boolean oshiReference = false;
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
@@ -64,6 +100,12 @@ public final class ModdedArtifactVerifier {
if (startsWithAny(name, BUNDLED_RUNTIME_PREFIXES)) {
bundledRuntimeEntries.add(name);
}
if (name.startsWith(ASM_PREFIX)) {
asmEntries.add(name);
}
if (name.startsWith(NESTED_JAR_DIRECTORY) && name.endsWith(".jar")) {
shippedNestedJars.add(name);
}
if (isNestedJar(name)) {
inspectNestedJar(jar, entry, bundledRuntimeEntries, privateReferenceClasses);
}
@@ -71,7 +113,8 @@ public final class ModdedArtifactVerifier {
continue;
}
String bytecode = readClass(jar, entry);
byte[] classFile = readEntryBytes(jar, entry);
String bytecode = new String(classFile, StandardCharsets.ISO_8859_1);
if (containsAny(bytecode, PRIVATE_REFERENCE_PREFIXES)) {
privateReferenceClasses.add(name);
}
@@ -85,6 +128,11 @@ public final class ModdedArtifactVerifier {
}
}
}
if (!name.startsWith(BUKKIT_PLATFORM_PREFIX)
&& !bukkitSupertypeBaseline.contains(name)
&& hasBukkitSupertype(classFile)) {
bukkitSupertypeClasses.add(name);
}
}
if (!bundledRuntimeEntries.isEmpty()) {
@@ -101,14 +149,181 @@ public final class ModdedArtifactVerifier {
if (!oshiReference) {
throw new GradleException(artifact.getName() + " does not link hardware diagnostics to Minecraft's OSHI runtime");
}
if (!asmEntries.isEmpty()) {
throw new GradleException(artifact.getName() + " ships unrelocated ASM: " + preview(asmEntries));
}
if (!bukkitSupertypeClasses.isEmpty()) {
throw new GradleException(artifact.getName() + " ships classes with an org.bukkit supertype outside "
+ BUKKIT_PLATFORM_PREFIX + ": " + preview(bukkitSupertypeClasses));
}
verifyNestedJarDeclarations(artifact, jar, shippedNestedJars);
verifyMixinCompatibilityLevels(artifact, jar);
} catch (IOException e) {
throw new GradleException("Unable to verify modded Iris artifact " + artifact.getAbsolutePath(), e);
}
}
private static String readClass(JarFile jar, JarEntry entry) throws IOException {
private static void verifyNestedJarDeclarations(File artifact, JarFile jar, Set<String> shippedNestedJars)
throws IOException {
String metadata = readEntryText(jar, FABRIC_METADATA);
if (metadata == null) {
if (!shippedNestedJars.isEmpty()) {
throw new GradleException(artifact.getName() + " ships nested jars but has no " + FABRIC_METADATA
+ " to declare them: " + preview(shippedNestedJars));
}
return;
}
Set<String> declared = collectQuotedFiles(extractJsonArray(metadata, "jars"), ".jar");
Set<String> undeclared = new LinkedHashSet<>(shippedNestedJars);
undeclared.removeAll(declared);
if (!undeclared.isEmpty()) {
throw new GradleException(artifact.getName() + " ships nested jars that " + FABRIC_METADATA
+ " does not declare: " + preview(undeclared));
}
Set<String> missing = new LinkedHashSet<>(declared);
missing.removeAll(shippedNestedJars);
if (!missing.isEmpty()) {
throw new GradleException(artifact.getName() + " declares nested jars in " + FABRIC_METADATA
+ " that it does not ship: " + preview(missing));
}
}
private static void verifyMixinCompatibilityLevels(File artifact, JarFile jar) throws IOException {
Set<String> configs = new LinkedHashSet<>();
Manifest manifest = jar.getManifest();
if (manifest != null) {
String declared = manifest.getMainAttributes().getValue(MIXIN_CONFIGS_ATTRIBUTE);
if (declared != null) {
for (String config : declared.split(",")) {
String trimmed = config.trim();
if (!trimmed.isEmpty()) {
configs.add(trimmed);
}
}
}
}
String neoforgeMetadata = readEntryText(jar, NEOFORGE_METADATA);
if (neoforgeMetadata != null) {
Matcher matcher = TOML_CONFIG_ASSIGNMENT.matcher(neoforgeMetadata);
while (matcher.find()) {
configs.add(matcher.group(1));
}
}
String fabricMetadata = readEntryText(jar, FABRIC_METADATA);
if (fabricMetadata != null) {
configs.addAll(collectQuotedFiles(extractJsonArray(fabricMetadata, "mixins"), ".json"));
}
if (configs.isEmpty()) {
throw new GradleException(artifact.getName() + " registers no mixin configs; expected the "
+ MIXIN_CONFIGS_ATTRIBUTE + " manifest attribute (Forge), [[mixins]] (NeoForge), or a mixins "
+ "array (Fabric)");
}
for (String config : configs) {
String body = readEntryText(jar, config);
if (body == null) {
throw new GradleException(artifact.getName() + " registers mixin config " + config
+ " which is not in the jar");
}
Matcher matcher = COMPATIBILITY_LEVEL.matcher(body);
if (!matcher.find()) {
continue;
}
String level = matcher.group(1);
Matcher value = COMPATIBILITY_LEVEL_VALUE.matcher(level);
if (!value.matches()) {
throw new GradleException(artifact.getName() + " mixin config " + config
+ " has unparseable compatibilityLevel " + level);
}
if (Integer.parseInt(value.group(1)) > MAX_MIXIN_COMPATIBILITY_LEVEL) {
throw new GradleException(artifact.getName() + " mixin config " + config
+ " requests compatibilityLevel " + level + "; upstream Mixin 0.8.7 (Forge) tops out at JAVA_"
+ MAX_MIXIN_COMPATIBILITY_LEVEL);
}
}
}
private static boolean hasBukkitSupertype(byte[] classFile) {
ClassReader reader;
try {
reader = new ClassReader(classFile);
} catch (RuntimeException e) {
// Class file version newer than the bundled ASM. Nothing to assert about it.
return false;
}
String superName = reader.getSuperName();
if (superName != null && superName.startsWith(BUKKIT_TYPE_PREFIX)) {
return true;
}
for (String candidate : reader.getInterfaces()) {
if (candidate.startsWith(BUKKIT_TYPE_PREFIX)) {
return true;
}
}
return false;
}
/**
* Returns the balanced {@code [...]} block that follows {@code "key"}, or an empty string.
*/
private static String extractJsonArray(String json, String key) {
int keyIndex = json.indexOf('"' + key + '"');
if (keyIndex < 0) {
return "";
}
int open = json.indexOf('[', keyIndex);
if (open < 0) {
return "";
}
int depth = 0;
for (int i = open; i < json.length(); i++) {
char c = json.charAt(i);
if (c == '[') {
depth++;
} else if (c == ']') {
depth--;
if (depth == 0) {
return json.substring(open, i + 1);
}
}
}
return "";
}
private static Set<String> collectQuotedFiles(String region, String suffix) {
Set<String> found = new LinkedHashSet<>();
Matcher matcher = QUOTED_JSON_FILE.matcher(region);
while (matcher.find()) {
String value = matcher.group(1);
if (value.endsWith(suffix)) {
found.add(value);
}
}
return found;
}
private static String readEntryText(JarFile jar, String name) throws IOException {
JarEntry entry = jar.getJarEntry(name);
if (entry == null) {
return null;
}
return new String(readEntryBytes(jar, entry), StandardCharsets.UTF_8);
}
private static byte[] readEntryBytes(JarFile jar, JarEntry entry) throws IOException {
try (InputStream input = jar.getInputStream(entry)) {
return new String(input.readAllBytes(), StandardCharsets.ISO_8859_1);
return input.readAllBytes();
}
}
@@ -163,7 +378,7 @@ public final class ModdedArtifactVerifier {
private static boolean isNestedJar(String name) {
return name.endsWith(".jar")
&& (name.startsWith("META-INF/jars/") || name.startsWith("META-INF/jarjar/"));
&& (name.startsWith(NESTED_JAR_DIRECTORY) || name.startsWith("META-INF/jarjar/"));
}
private static boolean isNamedRuntimeLibrary(String name) {
@@ -2,6 +2,8 @@ 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 java.io.ByteArrayOutputStream;
import java.io.File;
@@ -10,8 +12,11 @@ import java.nio.charset.StandardCharsets;
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;
@@ -20,6 +25,9 @@ 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/util/common/misc/getHardware.class";
private static final String MIXIN_CONFIG = "irisworldgen.entity.mixins.json";
private static final String CLIENT_MIXIN_CONFIG = "irisworldgen.client.mixins.json";
private static final String NEOFORGE_METADATA = "META-INF/neoforge.mods.toml";
private static final List<String> REQUIRED_ENTRIES = List.of(METADATA, CODEC_CLASS, HARDWARE_CLASS);
private static final byte[] INTERNAL_CODEC = (
"net/jpountz/lz4/LZ4BlockInputStream net/jpountz/lz4/LZ4BlockOutputStream"
@@ -96,17 +104,224 @@ public class ModdedArtifactVerifierTest {
assertTrue(failure.getMessage().contains("is missing " + METADATA));
}
@Test
public void rejectsUnrelocatedAsm() throws Exception {
Map<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/engine/platform/BukkitChunkGenerator.class",
classExtending("art/arcane/iris/engine/platform/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<String, byte[]> 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<String, byte[]> 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 acceptsBaselinedBukkitSupertype() throws Exception {
String entryName = "art/arcane/iris/engine/platform/BukkitChunkGenerator.class";
Map<String, byte[]> entries = validEntries();
entries.put(entryName, classExtending("art/arcane/iris/engine/platform/BukkitChunkGenerator",
"org/bukkit/generator/ChunkGenerator"));
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES, Set.of(entryName));
}
private Map<String, byte[]> validEntries() {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put(METADATA, new byte[]{0});
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<String, byte[]> entries) throws Exception {
return createArtifact(entries, null);
}
private File createArtifact(Map<String, byte[]> entries, Manifest manifest) throws Exception {
File artifact = temporaryFolder.newFile("artifact-" + System.nanoTime() + ".jar");
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) {
try (FileOutputStream file = new FileOutputStream(artifact);
JarOutputStream output = manifest == null
? new JarOutputStream(file)
: new JarOutputStream(file, manifest)) {
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
output.putNextEntry(new JarEntry(entry.getKey()));
output.write(entry.getValue());