mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
F
This commit is contained in:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user