From 0837842f5cab394e003cae8a690405bd93fc0998 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sat, 26 Sep 2026 03:54:14 -0400 Subject: [PATCH] Updated Iris packaging - Version is 4.1.2-26.2 across platform builds - Plugin builds use shared automatic jar selection - Native adapters remain bundled in selected artifacts - Plugin export tasks copy the selected artifact - The previous plugin packer is removed - The agent tracks primary storage closure during Spigot shutdown --- adapters/fabric/build.gradle | 2 +- adapters/forge/build.gradle | 2 +- adapters/neoforge/build.gradle | 2 +- build.gradle | 28 +- buildSrc/build.gradle | 1 - buildSrc/src/main/java/PackedJar.java | 376 ----------------- buildSrc/src/test/java/PackedJarTest.java | 399 ------------------ .../arcane/iris/platform/agent/Installer.java | 18 + .../agent/ServerStorageBoundaryTest.java | 49 +++ .../art/arcane/iris/platform/agent/Agent.java | 3 + gradle.properties | 2 +- gradle/packed-jar.gradle | 59 --- .../platform/bootstrap/PackedRuntime.java | 213 ---------- .../platform/bootstrap/PackedRuntimeTest.java | 171 -------- 14 files changed, 96 insertions(+), 1229 deletions(-) delete mode 100644 buildSrc/src/main/java/PackedJar.java delete mode 100644 buildSrc/src/test/java/PackedJarTest.java create mode 100644 core/agent/src/test/java/art/arcane/iris/platform/agent/ServerStorageBoundaryTest.java delete mode 100644 gradle/packed-jar.gradle delete mode 100644 packaging/xz/src/main/java/art/arcane/iris/platform/bootstrap/PackedRuntime.java delete mode 100644 packaging/xz/src/test/java/art/arcane/iris/platform/bootstrap/PackedRuntimeTest.java diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle index d37298015..f055c2a72 100644 --- a/adapters/fabric/build.gradle +++ b/adapters/fabric/build.gradle @@ -59,7 +59,7 @@ apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } -String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.1-26.2')) +String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.2-26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')) diff --git a/adapters/forge/build.gradle b/adapters/forge/build.gradle index fc24be777..a10f2349f 100644 --- a/adapters/forge/build.gradle +++ b/adapters/forge/build.gradle @@ -59,7 +59,7 @@ apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } -String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.1-26.2')) +String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.2-26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.2-65.1.1')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')) diff --git a/adapters/neoforge/build.gradle b/adapters/neoforge/build.gradle index 0d6023e6e..b995a63d0 100644 --- a/adapters/neoforge/build.gradle +++ b/adapters/neoforge/build.gradle @@ -59,7 +59,7 @@ apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } -String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.1-26.2')) +String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.1.2-26.2')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2')) String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.59')) String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT')) diff --git a/build.gradle b/build.gradle index d42637e2f..345871505 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,4 @@ +import art.arcane.volmit.packaging.PackedJar import art.arcane.volmit.packaging.PackagingMode import art.arcane.volmit.packaging.NativeDevelopmentTargets import io.papermc.paperweight.userdev.PaperweightUser @@ -63,7 +64,7 @@ interface IrisFileOperations { IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations) group = 'art.arcane' -version = providers.gradleProperty('irisVersion').getOrElse('4.1.1-26.2') +version = providers.gradleProperty('irisVersion').getOrElse('4.1.2-26.2') project(':core') { group = 'art.arcane' @@ -255,8 +256,9 @@ tasks.named('jar', Jar).configure { tasks.register('iris', Copy) { group = 'iris' - dependsOn('jar') - from(layout.buildDirectory.file("libs/${bukkitArtifactName}")) + dependsOn('packedJar') + from({ tasks.named('packedJar').get().outputJar }) + rename { String ignored -> bukkitArtifactName } into(layout.buildDirectory.dir('consumer')) doFirst { irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(staleConsumerBukkitJars) } @@ -294,8 +296,9 @@ tasks.register('buildSwiftSwamp') { tasks.register('buildBukkit', Copy) { group = 'iris' - dependsOn('jar') - from(layout.buildDirectory.file("libs/${bukkitArtifactName}")) + dependsOn('packedJar') + from({ tasks.named('packedJar').get().outputJar }) + rename { String ignored -> bukkitArtifactName } into(pluginOutDirectory) File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}.jar") doFirst { @@ -928,6 +931,8 @@ pluginPackaging { create('distribution') { taskName = 'jar' policyName = 'Iris Bukkit' + packed = true + packedJarAccessors = ['art/arcane/iris/platform/bukkit/plugin/VolmitPlugin#getJarFile'] stripDirectories = true stripLocalVariables = true releaseCompression = providers.gradleProperty('compactRelease') @@ -1126,4 +1131,15 @@ commands: } } -apply from: file('gradle/packed-jar.gradle') +tasks.withType(PackedJar).configureEach { + nativeManifestPath.set('META-INF/iris/native-runtime.properties') + nativeResourceDirectory.set('META-INF/iris/native/') + dependsOn(tasks.named('verifyBukkitArtifact'), configurations.nativeRuntime) + nativeArtifacts.set(providers.provider { + Map resolved = new LinkedHashMap<>() + configurations.nativeRuntime.incoming.artifacts.artifacts.each { artifact -> + resolved.put(artifact.id.componentIdentifier, artifact.file) + } + PackedJar.resolveNativeArtifacts(resolved) + }) +} diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index aadba42e8..67f86ee32 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -31,7 +31,6 @@ tasks.named('test').configure { dependencies { implementation(gradleApi()) implementation('org.ow2.asm:asm:9.10.1') - implementation('org.tukaani:xz:1.10') implementation('org.apache.commons:commons-compress:1.28.0') implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21') testImplementation('junit:junit:4.13.2') diff --git a/buildSrc/src/main/java/PackedJar.java b/buildSrc/src/main/java/PackedJar.java deleted file mode 100644 index 2d3405245..000000000 --- a/buildSrc/src/main/java/PackedJar.java +++ /dev/null @@ -1,376 +0,0 @@ -import org.gradle.api.DefaultTask; -import org.gradle.api.GradleException; -import org.gradle.api.artifacts.component.ComponentIdentifier; -import org.gradle.api.artifacts.component.ModuleComponentIdentifier; -import org.gradle.api.artifacts.component.ProjectComponentIdentifier; -import org.gradle.api.file.ConfigurableFileCollection; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.MapProperty; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFile; -import org.gradle.api.tasks.InputFiles; -import org.gradle.api.tasks.Internal; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.TaskAction; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.tukaani.xz.LZMA2Options; -import org.tukaani.xz.XZOutputStream; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HexFormat; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.stream.Stream; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public abstract class PackedJar extends DefaultTask { - static final String RUNTIME = "art/arcane/iris/platform/bootstrap/PackedRuntime"; - static final String PLUGIN_BASE = "art/arcane/iris/platform/bukkit/plugin/VolmitPlugin"; - static final String PAPER_LOADER = "art/arcane/iris/IrisPluginLoader"; - static final String PAPER_BOOTSTRAP = "art/arcane/iris/IrisBootstrap"; - static final String PAYLOAD = "META-INF/iris/runtime.jar.xz"; - static final String CHECKSUM = "META-INF/iris/runtime.sha256"; - static final String NATIVE_DIRECTORY = "META-INF/iris/native/"; - - @InputFile - @PathSensitive(PathSensitivity.NONE) - public abstract RegularFileProperty getInputJar(); - - @InputFiles - @PathSensitive(PathSensitivity.RELATIVE) - public abstract ConfigurableFileCollection getBootstrapClasses(); - - @InputFiles - @PathSensitive(PathSensitivity.NONE) - public abstract ConfigurableFileCollection getDecoderJars(); - - @Internal - public abstract MapProperty getNativeArtifacts(); - - @Input - public List getNativeModules() { - return getNativeArtifacts().get().keySet().stream().sorted().toList(); - } - - @InputFiles - @PathSensitive(PathSensitivity.NONE) - public Collection getNativeProviderJars() { - return getNativeArtifacts().get().values(); - } - - @OutputFile - public abstract RegularFileProperty getOutputJar(); - - @TaskAction - public void pack() throws IOException, NoSuchAlgorithmException { - Map bootstrap = new TreeMap<>(); - for (File directory : getBootstrapClasses()) { - try (Stream paths = Files.walk(directory.toPath())) { - for (Path path : paths.filter(Files::isRegularFile).sorted().toList()) { - bootstrap.put(directory.toPath().relativize(path).toString().replace(File.separatorChar, '/'), - Files.readAllBytes(path)); - } - } - } - for (File decoder : getDecoderJars()) { - for (Map.Entry entry : readJar(decoder.toPath()).entrySet()) { - if (entry.getKey().startsWith("org/tukaani/xz/") && entry.getKey().endsWith(".class")) { - bootstrap.put(entry.getKey(), entry.getValue()); - } - } - } - if (!bootstrap.containsKey(RUNTIME + ".class")) { - throw new IOException("Packed runtime bootstrap class is missing."); - } - Path source = getInputJar().get().getAsFile().toPath(); - Path output = getOutputJar().get().getAsFile().toPath(); - PackResult result = writePackedJar(source, output, bootstrap, getNativeArtifacts().get()); - getLogger().lifecycle("Packed {} classes into XZ; retained {} bootstrap classes. {} -> {} bytes", - result.totalClasses(), result.bootstrapClasses(), Files.size(source), Files.size(output)); - } - - static PackResult writePackedJar(Path source, Path output, Map bootstrap, - Map nativeArtifacts) - throws IOException, NoSuchAlgorithmException { - Map contents = readJar(source); - embedNativeArtifacts(contents, nativeArtifacts); - int classCount = (int) contents.keySet().stream().filter(name -> name.endsWith(".class")).count(); - for (String name : List.of(PLUGIN_BASE, PAPER_LOADER, PAPER_BOOTSTRAP)) { - byte[] original = contents.get(name + ".class"); - if (original == null) { - throw new IOException("Required bootstrap entry is missing: " + name); - } - contents.put(name + ".class", instrument(original, name.equals(PLUGIN_BASE))); - } - Set retained = bootstrapClosure(contents); - contents.putAll(bootstrap); - byte[] payload = writeJar(contents, true); - ByteArrayOutputStream compressed = new ByteArrayOutputStream(); - LZMA2Options options = new LZMA2Options(9); - options.setDictSize(Math.min(options.getDictSize(), Math.max(4096, payload.length))); - try (XZOutputStream xz = new XZOutputStream(compressed, options)) { - xz.write(payload); - } - Map outer = new TreeMap<>(); - for (Map.Entry entry : contents.entrySet()) { - if (entry.getKey().startsWith(NATIVE_DIRECTORY)) { - continue; - } - if (entry.getKey().endsWith(".class")) { - if (!retained.contains(entry.getKey())) { - continue; - } - } - outer.put(entry.getKey(), entry.getValue()); - } - outer.putAll(bootstrap); - outer.put(PAYLOAD, compressed.toByteArray()); - outer.put(CHECKSUM, (HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload)) + "\n") - .getBytes(StandardCharsets.US_ASCII)); - Files.createDirectories(output.toAbsolutePath().getParent()); - Files.write(output, writeJar(outer, false)); - return new PackResult(classCount, retained.size()); - } - - public static Map resolveNativeArtifacts(Map resolved) { - Map artifacts = new TreeMap<>(); - for (Map.Entry entry : resolved.entrySet()) { - String module; - if (entry.getKey() instanceof ModuleComponentIdentifier component - && component.getGroup().equals("com.github.VolmitSoftware.VolmLib")) { - module = component.getModule(); - } else if (entry.getKey() instanceof ProjectComponentIdentifier component) { - module = component.getProjectPath().substring(1).replace(':', '-'); - } else { - throw new GradleException("Unexpected native provider component: " + entry.getKey().getDisplayName()); - } - if (!module.matches("native-(common|v[0-9_]+R[0-9]+)")) { - throw new GradleException("Unexpected native provider module: " + module); - } - if (artifacts.put(module, entry.getValue()) != null) { - throw new GradleException("Duplicate native provider module: " + module); - } - } - return artifacts; - } - - private static void embedNativeArtifacts(Map contents, Map artifacts) - throws IOException, NoSuchAlgorithmException { - byte[] original = contents.get(NativeRuntimeArtifacts.MANIFEST); - if (original == null) { - throw new IOException("Missing native runtime dependency manifest"); - } - Properties manifest = new Properties(); - manifest.load(new ByteArrayInputStream(original)); - Set modules = new TreeSet<>(List.of(manifest.getProperty("modules", "").split(","))); - if (!modules.equals(artifacts.keySet()) || !modules.contains("native-common")) { - throw new IOException("Embedded native providers differ: " + artifacts.keySet() + "; expected " + modules); - } - List embeddedManifest = new ArrayList<>(); - embeddedManifest.add("modules=" + String.join(",", modules)); - embeddedManifest.add("storage=embedded"); - for (String module : modules) { - if (!module.matches("native-(common|v[0-9_]+R[0-9]+)")) { - throw new IOException("Invalid embedded native provider module: " + module); - } - byte[] bytes = Files.readAllBytes(artifacts.get(module).toPath()); - String digest = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)); - embeddedManifest.add(module + ".coordinate=com.github.VolmitSoftware.VolmLib:" + module + ":embedded-" + digest); - embeddedManifest.add(module + ".sha256=" + digest); - contents.put(NATIVE_DIRECTORY + module + ".jar", bytes); - } - contents.put(NativeRuntimeArtifacts.MANIFEST, - (String.join("\n", embeddedManifest) + "\n").getBytes(StandardCharsets.UTF_8)); - } - - static Map readJar(Path path) throws IOException { - Map contents = new TreeMap<>(); - try (ZipFile jar = new ZipFile(path.toFile())) { - for (ZipEntry entry : jar.stream().filter(entry -> !entry.isDirectory()).toList()) { - try (InputStream input = jar.getInputStream(entry)) { - contents.put(entry.getName(), input.readAllBytes()); - } - } - } - return contents; - } - - static byte[] writeJar(Map contents, boolean stored) throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - try (JarOutputStream jar = new JarOutputStream(bytes)) { - jar.setLevel(9); - for (Map.Entry entry : contents.entrySet()) { - JarEntry zipEntry = new JarEntry(entry.getKey()); - zipEntry.setTime(0L); - if (stored || entry.getKey().equals(PAYLOAD)) { - CRC32 crc = new CRC32(); - crc.update(entry.getValue()); - zipEntry.setMethod(ZipEntry.STORED); - zipEntry.setSize(entry.getValue().length); - zipEntry.setCompressedSize(entry.getValue().length); - zipEntry.setCrc(crc.getValue()); - } - jar.putNextEntry(zipEntry); - jar.write(entry.getValue()); - jar.closeEntry(); - } - } - return bytes.toByteArray(); - } - - static Set bootstrapClosure(Map contents) { - List roots = List.of("art/arcane/iris/Iris", PLUGIN_BASE, PAPER_LOADER, - PAPER_BOOTSTRAP, "art/arcane/volmlib/integration/ReloadAware"); - ArrayDeque pending = new ArrayDeque<>(roots); - for (String root : roots) { - byte[] bytes = contents.get(root + ".class"); - if (bytes != null) { - pending.addAll(ClassReferences.read(bytes)); - } - } - for (String entry : contents.keySet()) { - if (entry.startsWith("art/arcane/iris/util/slimjar/") && entry.endsWith(".class")) { - pending.add(entry.substring(0, entry.length() - 6)); - } - } - Set retained = new LinkedHashSet<>(); - while (!pending.isEmpty()) { - String name = pending.removeFirst(); - byte[] bytes = contents.get(name + ".class"); - if (bytes == null || !retained.add(name + ".class")) { - continue; - } - ClassReader reader = new ClassReader(bytes); - if (reader.getSuperName() != null) { - pending.add(reader.getSuperName()); - } - for (String implemented : reader.getInterfaces()) { - pending.add(implemented); - } - } - return retained; - } - - static byte[] instrument(byte[] original, boolean rewriteJarAccessor) { - ClassReader reader = new ClassReader(original); - ClassWriter writer = new ClassWriter(reader, 0); - reader.accept(new ClassVisitor(Opcodes.ASM9, writer) { - private boolean hasInitializer; - - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, - String[] exceptions) { - MethodVisitor method = super.visitMethod(access, name, descriptor, signature, exceptions); - if (reader.getClassName().equals(PAPER_LOADER) && name.equals("classloader") - && descriptor.equals("(Lio/papermc/paper/plugin/loader/PluginClasspathBuilder;)V")) { - return releaseTemporaryLoader(method); - } - if (rewriteJarAccessor && name.equals("getJarFile") && descriptor.equals("()Ljava/io/File;")) { - method.visitCode(); - method.visitMethodInsn(Opcodes.INVOKESTATIC, RUNTIME, "runtimeJar", "()Ljava/io/File;", false); - method.visitInsn(Opcodes.ARETURN); - method.visitMaxs(1, 1); - method.visitEnd(); - return null; - } - if (!name.equals("")) { - return method; - } - hasInitializer = true; - return new MethodVisitor(Opcodes.ASM9, method) { - @Override - public void visitCode() { - super.visitCode(); - visitMethodInsn(Opcodes.INVOKESTATIC, RUNTIME, "install", "()V", false); - } - }; - } - - @Override - public void visitEnd() { - if (!hasInitializer) { - MethodVisitor initializer = super.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null); - initializer.visitCode(); - initializer.visitMethodInsn(Opcodes.INVOKESTATIC, RUNTIME, "install", "()V", false); - initializer.visitInsn(Opcodes.RETURN); - initializer.visitMaxs(0, 0); - initializer.visitEnd(); - } - super.visitEnd(); - } - }, 0); - return writer.toByteArray(); - } - - private static MethodVisitor releaseTemporaryLoader(MethodVisitor method) { - return new MethodVisitor(Opcodes.ASM9, method) { - private final Label start = new Label(); - private final Label end = new Label(); - private final Label handler = new Label(); - private final Label completed = new Label(); - - @Override - public void visitCode() { - super.visitCode(); - super.visitLabel(start); - } - - @Override - public void visitInsn(int opcode) { - if (opcode == Opcodes.RETURN) { - super.visitJumpInsn(Opcodes.GOTO, completed); - } else { - super.visitInsn(opcode); - } - } - - @Override - public void visitMaxs(int maxStack, int maxLocals) { - super.visitLabel(end); - super.visitTryCatchBlock(start, end, handler, "java/lang/Throwable"); - super.visitLabel(handler); - super.visitFrame(Opcodes.F_FULL, 0, null, 1, new Object[]{"java/lang/Throwable"}); - super.visitInsn(Opcodes.DUP); - super.visitMethodInsn(Opcodes.INVOKESTATIC, RUNTIME, "releaseTemporary", "(Ljava/lang/Throwable;)V", false); - super.visitInsn(Opcodes.ATHROW); - super.visitLabel(completed); - super.visitFrame(Opcodes.F_FULL, 0, null, 0, null); - super.visitInsn(Opcodes.ACONST_NULL); - super.visitMethodInsn(Opcodes.INVOKESTATIC, RUNTIME, "releaseTemporary", "(Ljava/lang/Throwable;)V", false); - super.visitInsn(Opcodes.RETURN); - super.visitMaxs(Math.max(maxStack, 2), maxLocals); - } - }; - } - - record PackResult(int totalClasses, int bootstrapClasses) { - } -} diff --git a/buildSrc/src/test/java/PackedJarTest.java b/buildSrc/src/test/java/PackedJarTest.java deleted file mode 100644 index c78fd2b76..000000000 --- a/buildSrc/src/test/java/PackedJarTest.java +++ /dev/null @@ -1,399 +0,0 @@ -import org.gradle.api.artifacts.component.ProjectComponentIdentifier; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Label; -import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; -import org.tukaani.xz.XZInputStream; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.HexFormat; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.zip.ZipEntry; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class PackedJarTest { - @Rule - public TemporaryFolder directory = new TemporaryFolder(); - - @Test - public void preservesAllRuntimeEntriesAndPublishesTheirChecksum() throws Exception { - Path original = directory.newFile("original.jar").toPath(); - Path packed = directory.getRoot().toPath().resolve("packed.jar"); - Map contents = new LinkedHashMap<>(); - contents.put(PackedJar.PLUGIN_BASE + ".class", pluginBase()); - contents.put(PackedJar.PAPER_LOADER + ".class", simpleClass(PackedJar.PAPER_LOADER, "java/lang/Object")); - contents.put(PackedJar.PAPER_BOOTSTRAP + ".class", simpleClass(PackedJar.PAPER_BOOTSTRAP, "java/lang/Object")); - contents.put("art/arcane/iris/command/Example.class", simpleClass("art/arcane/iris/command/Example", "java/lang/Object")); - contents.put("plugin.yml", "main: art.arcane.iris.Iris\n".getBytes(StandardCharsets.UTF_8)); - List modules = List.of("native-common", "native-v26_2_R1", "native-v26_3_R1"); - contents.put(NativeRuntimeArtifacts.MANIFEST, - ("modules=" + String.join(",", modules) + "\nrepository=http://127.0.0.1:8765/\n") - .getBytes(StandardCharsets.UTF_8)); - Map nativeArtifacts = new LinkedHashMap<>(); - for (String module : modules) { - Path artifact = directory.newFile(module + ".jar").toPath(); - Files.write(artifact, PackedJar.writeJar(Map.of("provider.txt", module.getBytes(StandardCharsets.UTF_8)), false)); - nativeArtifacts.put(module, artifact.toFile()); - } - Files.write(original, PackedJar.writeJar(contents, false)); - - Map bootstrap = Map.of( - PackedJar.RUNTIME + ".class", simpleClass(PackedJar.RUNTIME, "java/lang/Object"), - PackedJar.RUNTIME + "$RuntimeArchive.class", simpleClass(PackedJar.RUNTIME + "$RuntimeArchive", "java/lang/Object"), - "org/tukaani/xz/XZInputStream.class", simpleClass("org/tukaani/xz/XZInputStream", "java/lang/Object")); - PackedJar.PackResult result = PackedJar.writePackedJar(original, packed, bootstrap, nativeArtifacts); - assertEquals(4, result.totalClasses()); - Map outer = PackedJar.readJar(packed); - assertFalse(outer.containsKey("art/arcane/iris/command/Example.class")); - assertArrayEquals(contents.get("plugin.yml"), outer.get("plugin.yml")); - byte[] payload; - try (XZInputStream xz = new XZInputStream(new ByteArrayInputStream(outer.get(PackedJar.PAYLOAD)))) { - payload = xz.readAllBytes(); - } - assertEquals(HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(payload)), - new String(outer.get(PackedJar.CHECKSUM), StandardCharsets.US_ASCII).trim()); - Map extracted = new LinkedHashMap<>(); - try (JarInputStream jar = new JarInputStream(new ByteArrayInputStream(payload))) { - for (JarEntry entry = jar.getNextJarEntry(); entry != null; entry = jar.getNextJarEntry()) { - assertEquals(ZipEntry.STORED, entry.getMethod()); - extracted.put(entry.getName(), jar.readAllBytes()); - } - } - Map expected = new LinkedHashMap<>(contents); - expected.putAll(bootstrap); - for (String module : modules) { - expected.put(PackedJar.NATIVE_DIRECTORY + module + ".jar", Files.readAllBytes(nativeArtifacts.get(module).toPath())); - } - assertEquals(expected.keySet(), extracted.keySet()); - Properties nativeManifest = new Properties(); - nativeManifest.load(new ByteArrayInputStream(extracted.get(NativeRuntimeArtifacts.MANIFEST))); - assertEquals(String.join(",", modules), nativeManifest.getProperty("modules")); - assertEquals("embedded", nativeManifest.getProperty("storage")); - assertNull(nativeManifest.getProperty("repository")); - assertFalse(new String(extracted.get(NativeRuntimeArtifacts.MANIFEST), StandardCharsets.UTF_8).contains("127.0.0.1")); - assertArrayEquals(extracted.get(NativeRuntimeArtifacts.MANIFEST), outer.get(NativeRuntimeArtifacts.MANIFEST)); - for (String module : modules) { - String path = PackedJar.NATIVE_DIRECTORY + module + ".jar"; - byte[] artifact = Files.readAllBytes(nativeArtifacts.get(module).toPath()); - String digest = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(artifact)); - assertArrayEquals(artifact, extracted.get(path)); - assertFalse(outer.containsKey(path)); - assertEquals(digest, nativeManifest.getProperty(module + ".sha256")); - assertEquals("com.github.VolmitSoftware.VolmLib:" + module + ":embedded-" + digest, - nativeManifest.getProperty(module + ".coordinate")); - } - for (String name : bootstrap.keySet()) { - assertArrayEquals(bootstrap.get(name), extracted.get(name)); - assertArrayEquals(bootstrap.get(name), outer.get(name)); - } - for (String name : contents.keySet()) { - if (!name.equals(NativeRuntimeArtifacts.MANIFEST) - && !List.of(PackedJar.PLUGIN_BASE, PackedJar.PAPER_LOADER, PackedJar.PAPER_BOOTSTRAP).contains( - name.replace(".class", ""))) { - assertArrayEquals(contents.get(name), extracted.get(name)); - } - } - for (String name : List.of(PackedJar.PLUGIN_BASE, PackedJar.PAPER_LOADER, PackedJar.PAPER_BOOTSTRAP)) { - assertArrayEquals(extracted.get(name + ".class"), outer.get(name + ".class")); - assertEquals(1, installationCalls(extracted.get(name + ".class"))); - } - } - - @Test - public void rejectsMissingOrUnexpectedEmbeddedNativeProviders() throws Exception { - Path original = directory.newFile("original.jar").toPath(); - Path packed = directory.getRoot().toPath().resolve("packed.jar"); - byte[] manifest = "modules=native-common,native-v26_2_R1,native-v26_3_R1\n".getBytes(StandardCharsets.UTF_8); - Files.write(original, PackedJar.writeJar(Map.of(NativeRuntimeArtifacts.MANIFEST, manifest), false)); - File common = directory.newFile("common.jar"); - assertThrows(IOException.class, () -> PackedJar.writePackedJar(original, packed, Map.of(), - Map.of("native-common", common, "native-v26_2_R1", common))); - assertThrows(IOException.class, () -> PackedJar.writePackedJar(original, packed, Map.of(), - Map.of("native-common", common, "native-v26_2_R1", common, "native-v26_3_R1", common, "native-other", common))); - assertFalse(Files.exists(packed)); - } - - @Test - public void resolvesCompositeNativeModulesWithoutParsingArtifactFileNames() { - File common = new File("arbitrary-common.jar"); - File provider = new File("arbitrary-provider.jar"); - assertEquals(Map.of("native-common", common, "native-v26_2_R1", provider), - PackedJar.resolveNativeArtifacts(Map.of(project(":native-common"), common, - project(":native:v26_2_R1"), provider))); - } - - private static ProjectComponentIdentifier project(String path) { - return (ProjectComponentIdentifier) Proxy.newProxyInstance(PackedJarTest.class.getClassLoader(), - new Class[]{ProjectComponentIdentifier.class}, (proxy, method, arguments) -> switch (method.getName()) { - case "getProjectPath", "getDisplayName", "toString" -> path; - case "hashCode" -> System.identityHashCode(proxy); - case "equals" -> proxy == arguments[0]; - default -> throw new UnsupportedOperationException(method.getName()); - }); - } - - @Test - public void installsBeforeExistingInitializationAndRedirectsTheScannerJar() { - List instructions = new ArrayList<>(); - new ClassReader(PackedJar.instrument(pluginBase(), true)).accept(new ClassVisitor(Opcodes.ASM9) { - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, - String[] exceptions) { - return new MethodVisitor(Opcodes.ASM9) { - @Override - public void visitMethodInsn(int opcode, String owner, String methodName, String methodDescriptor, - boolean isInterface) { - instructions.add(name + ":" + owner + "." + methodName); - } - - @Override - public void visitLdcInsn(Object value) { - instructions.add(name + ":" + value); - } - }; - } - }, 0); - assertEquals(List.of(":" + PackedJar.RUNTIME + ".install", ":existing", - "getJarFile:" + PackedJar.RUNTIME + ".runtimeJar"), instructions); - } - - @Test - public void retainsEntrypointReferencesAndHierarchyWithoutRecursiveMemberReferences() { - Map contents = new LinkedHashMap<>(); - ClassWriter iris = new ClassWriter(0); - iris.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, "art/arcane/iris/Iris", null, "example/Base", null); - iris.visitField(Opcodes.ACC_PUBLIC, "field", "Lexample/Signature;", null, null).visitEnd(); - MethodVisitor method = iris.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "later", "()V", null, null); - method.visitCode(); - method.visitMethodInsn(Opcodes.INVOKESTATIC, "example/Body", "later", "()V", false); - method.visitInsn(Opcodes.RETURN); - method.visitMaxs(0, 0); - method.visitEnd(); - iris.visitEnd(); - contents.put("art/arcane/iris/Iris.class", iris.toByteArray()); - for (String name : List.of("example/Base", "example/Signature", "example/Body")) { - contents.put(name + ".class", simpleClass(name, "java/lang/Object")); - } - ClassWriter signature = new ClassWriter(0); - signature.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, "example/Signature", null, "example/Ancestor", null); - signature.visitField(Opcodes.ACC_PUBLIC, "field", "Lexample/Chained;", null, null).visitEnd(); - signature.visitEnd(); - contents.put("example/Signature.class", signature.toByteArray()); - contents.put("example/Ancestor.class", simpleClass("example/Ancestor", "java/lang/Object")); - contents.put("example/Chained.class", simpleClass("example/Chained", "java/lang/Object")); - Set retained = PackedJar.bootstrapClosure(contents); - assertTrue(retained.contains("example/Base.class")); - assertTrue(retained.contains("example/Signature.class")); - assertTrue(retained.contains("example/Body.class")); - assertTrue(retained.contains("example/Ancestor.class")); - assertFalse(retained.contains("example/Chained.class")); - } - - @Test - public void releasesTemporaryLoaderAfterSuccessAndPreservesThrownFailure() throws Exception { - assertLoaderCleanup(false); - } - - @Test - public void releasesTemporaryLoaderWithDiscardedParameterFrameLocals() throws Exception { - assertLoaderCleanup(true); - } - - private static void assertLoaderCleanup(boolean discardFrameLocals) throws Exception { - String builderName = "io/papermc/paper/plugin/loader/PluginClasspathBuilder"; - byte[] original = loaderFixture(builderName); - if (discardFrameLocals) { - original = discardFrameLocals(original); - } - FixtureLoader fixture = new FixtureLoader(Map.of( - builderName, simpleClass(builderName, "java/lang/Object"), - PackedJar.RUNTIME, runtimeFixture(), - PackedJar.PAPER_LOADER, PackedJar.instrument(original, false))); - Class runtime = fixture.loadClass(PackedJar.RUNTIME.replace('/', '.')); - Class loader = fixture.loadClass(PackedJar.PAPER_LOADER.replace('/', '.')); - Object instance = loader.getConstructor().newInstance(); - Method invoke = loader.getMethod("classloader", fixture.loadClass(builderName.replace('/', '.'))); - invoke.invoke(instance, new Object[]{null}); - assertEquals(1, runtime.getField("releases").getInt(null)); - assertNull(runtime.getField("released").get(null)); - Throwable failure = new IllegalStateException("load failed"); - runtime.getField("failure").set(null, failure); - try { - invoke.invoke(instance, new Object[]{null}); - fail("Expected the original loader failure"); - } catch (InvocationTargetException caught) { - assertSame(failure, caught.getCause()); - } - assertEquals(2, runtime.getField("releases").getInt(null)); - assertSame(failure, runtime.getField("released").get(null)); - } - - private static byte[] discardFrameLocals(byte[] original) { - ClassWriter writer = new ClassWriter(0); - new ClassReader(original).accept(new ClassVisitor(Opcodes.ASM9, writer) { - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, - String[] exceptions) { - MethodVisitor method = super.visitMethod(access, name, descriptor, signature, exceptions); - if (!name.equals("classloader")) { - return method; - } - return new MethodVisitor(Opcodes.ASM9, method) { - @Override - public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) { - super.visitFrame(Opcodes.F_FULL, 0, null, numStack, stack); - } - }; - } - }, ClassReader.EXPAND_FRAMES); - return writer.toByteArray(); - } - - private static int installationCalls(byte[] bytes) { - List calls = new ArrayList<>(); - new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) { - @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, - String[] exceptions) { - return new MethodVisitor(Opcodes.ASM9) { - @Override - public void visitMethodInsn(int opcode, String owner, String methodName, String methodDescriptor, - boolean isInterface) { - if (name.equals("") && owner.equals(PackedJar.RUNTIME) && methodName.equals("install")) { - calls.add(methodName); - } - } - }; - } - }, 0); - return calls.size(); - } - - private static byte[] runtimeFixture() { - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, PackedJar.RUNTIME, null, "java/lang/Object", null); - for (String field : List.of("failure", "released")) { - writer.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, field, "Ljava/lang/Throwable;", null, null).visitEnd(); - } - writer.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "releases", "I", null, null).visitEnd(); - MethodVisitor install = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "install", "()V", null, null); - install.visitCode(); - install.visitInsn(Opcodes.RETURN); - install.visitMaxs(0, 0); - install.visitEnd(); - MethodVisitor release = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "releaseTemporary", - "(Ljava/lang/Throwable;)V", null, null); - release.visitCode(); - release.visitVarInsn(Opcodes.ALOAD, 0); - release.visitFieldInsn(Opcodes.PUTSTATIC, PackedJar.RUNTIME, "released", "Ljava/lang/Throwable;"); - release.visitFieldInsn(Opcodes.GETSTATIC, PackedJar.RUNTIME, "releases", "I"); - release.visitInsn(Opcodes.ICONST_1); - release.visitInsn(Opcodes.IADD); - release.visitFieldInsn(Opcodes.PUTSTATIC, PackedJar.RUNTIME, "releases", "I"); - release.visitInsn(Opcodes.RETURN); - release.visitMaxs(0, 0); - release.visitEnd(); - writer.visitEnd(); - return writer.toByteArray(); - } - - private static byte[] loaderFixture(String builderName) { - ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); - writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, PackedJar.PAPER_LOADER, null, "java/lang/Object", null); - MethodVisitor constructor = writer.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); - constructor.visitCode(); - constructor.visitVarInsn(Opcodes.ALOAD, 0); - constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); - constructor.visitInsn(Opcodes.RETURN); - constructor.visitMaxs(0, 0); - constructor.visitEnd(); - MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC, "classloader", "(L" + builderName + ";)V", null, null); - method.visitCode(); - method.visitFieldInsn(Opcodes.GETSTATIC, PackedJar.RUNTIME, "failure", "Ljava/lang/Throwable;"); - method.visitInsn(Opcodes.DUP); - Label success = new Label(); - method.visitJumpInsn(Opcodes.IFNULL, success); - method.visitInsn(Opcodes.ATHROW); - method.visitLabel(success); - method.visitInsn(Opcodes.POP); - method.visitInsn(Opcodes.RETURN); - method.visitMaxs(0, 0); - method.visitEnd(); - writer.visitEnd(); - return writer.toByteArray(); - } - - private static byte[] simpleClass(String name, String parent) { - ClassWriter writer = new ClassWriter(0); - writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, name, null, parent, null); - writer.visitEnd(); - return writer.toByteArray(); - } - - private static byte[] pluginBase() { - ClassWriter writer = new ClassWriter(0); - writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, PackedJar.PLUGIN_BASE, null, "java/lang/Object", null); - MethodVisitor initializer = writer.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null); - initializer.visitCode(); - initializer.visitLdcInsn("existing"); - initializer.visitInsn(Opcodes.POP); - initializer.visitInsn(Opcodes.RETURN); - initializer.visitMaxs(1, 0); - initializer.visitEnd(); - MethodVisitor accessor = writer.visitMethod(Opcodes.ACC_PUBLIC, "getJarFile", "()Ljava/io/File;", null, null); - accessor.visitCode(); - accessor.visitInsn(Opcodes.ACONST_NULL); - accessor.visitInsn(Opcodes.ARETURN); - accessor.visitMaxs(1, 1); - accessor.visitEnd(); - writer.visitEnd(); - return writer.toByteArray(); - } - - private static final class FixtureLoader extends ClassLoader { - private final Map classes; - - private FixtureLoader(Map classes) { - super(PackedJarTest.class.getClassLoader()); - this.classes = classes; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - byte[] bytes = classes.get(name.replace('.', '/')); - if (bytes == null) { - throw new ClassNotFoundException(name); - } - return defineClass(name, bytes, 0, bytes.length); - } - } -} diff --git a/core/agent/src/main/java/art/arcane/iris/platform/agent/Installer.java b/core/agent/src/main/java/art/arcane/iris/platform/agent/Installer.java index 13c62424a..86b24d0c9 100644 --- a/core/agent/src/main/java/art/arcane/iris/platform/agent/Installer.java +++ b/core/agent/src/main/java/art/arcane/iris/platform/agent/Installer.java @@ -6,10 +6,28 @@ import java.io.IOException; import java.util.IdentityHashMap; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; public class Installer { private static volatile Instrumentation instrumentation; private static final Map retainedClassLoaders = new IdentityHashMap<>(); + private static final Map serverStorage = new IdentityHashMap<>(); + + public static synchronized AtomicBoolean trackServerStorage(Object storage) { + return serverStorage.computeIfAbsent(Objects.requireNonNull(storage, "Server storage"), + ignored -> new AtomicBoolean()); + } + + public static synchronized void serverStorageClosed(Object storage) { + AtomicBoolean closed = serverStorage.get(storage); + if (closed != null) { + closed.set(true); + } + } + + public static synchronized void releaseServerStorage(Object storage) { + serverStorage.remove(storage); + } public static synchronized void retainClassLoader(ClassLoader loader) { ClassLoader requiredLoader = Objects.requireNonNull(loader, "Plugin class loader"); diff --git a/core/agent/src/test/java/art/arcane/iris/platform/agent/ServerStorageBoundaryTest.java b/core/agent/src/test/java/art/arcane/iris/platform/agent/ServerStorageBoundaryTest.java new file mode 100644 index 000000000..a3003a345 --- /dev/null +++ b/core/agent/src/test/java/art/arcane/iris/platform/agent/ServerStorageBoundaryTest.java @@ -0,0 +1,49 @@ +package art.arcane.iris.platform.agent; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class ServerStorageBoundaryTest { + @Test + public void closingEqualStorageDoesNotCompleteTrackedStorage() { + Storage primary = new Storage(1); + Storage other = new Storage(1); + AtomicBoolean closed = Installer.trackServerStorage(primary); + try { + assertSame(closed, Installer.trackServerStorage(primary)); + Installer.serverStorageClosed(other); + assertFalse(closed.get()); + Installer.serverStorageClosed(primary); + assertTrue(closed.get()); + } finally { + Installer.releaseServerStorage(primary); + } + } + + @Test + public void releasedStorageNoLongerReceivesCloseSignals() { + Storage storage = new Storage(1); + AtomicBoolean released = Installer.trackServerStorage(storage); + Installer.releaseServerStorage(storage); + Installer.serverStorageClosed(storage); + assertFalse(released.get()); + AtomicBoolean current = Installer.trackServerStorage(storage); + try { + assertNotSame(released, current); + Installer.serverStorageClosed(storage); + assertTrue(current.get()); + assertFalse(released.get()); + } finally { + Installer.releaseServerStorage(storage); + } + } + + private record Storage(int value) { + } +} diff --git a/core/src/main/java/art/arcane/iris/platform/agent/Agent.java b/core/src/main/java/art/arcane/iris/platform/agent/Agent.java index bd3c5640b..8f576639f 100644 --- a/core/src/main/java/art/arcane/iris/platform/agent/Agent.java +++ b/core/src/main/java/art/arcane/iris/platform/agent/Agent.java @@ -29,6 +29,9 @@ public class Agent { installer.getMethod("retainClassLoader", ClassLoader.class); installer.getMethod("deferClassLoaderClose", ClassLoader.class); installer.getMethod("releaseClassLoader", ClassLoader.class); + installer.getMethod("trackServerStorage", Object.class); + installer.getMethod("serverStorageClosed", Object.class); + installer.getMethod("releaseServerStorage", Object.class); } public static void retainClassLoader(ClassLoader loader) { diff --git a/gradle.properties b/gradle.properties index 3c685a841..15d565080 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF org.gradle.caching=true org.gradle.configureondemand=false -irisVersion=4.1.1-26.2 +irisVersion=4.1.2-26.2 minecraftVersion=26.2 # Bukkit plugin api-version: lowest supported Minecraft release line so one artifact loads on 26.1.2 and 26.2 apiVersion=26.1 diff --git a/gradle/packed-jar.gradle b/gradle/packed-jar.gradle deleted file mode 100644 index b7a55318d..000000000 --- a/gradle/packed-jar.gradle +++ /dev/null @@ -1,59 +0,0 @@ -import org.gradle.api.tasks.compile.JavaCompile -import org.gradle.api.artifacts.component.ComponentIdentifier - -configurations { - packedXz -} - -dependencies { - packedXz('org.tukaani:xz:1.10') -} - -def originalJar = tasks.named('jar', Jar) -sourceSets { - packedBootstrap { - java.srcDir('packaging/xz/src/main/java') - compileClasspath = files(originalJar.flatMap { it.archiveFile }) + configurations.packedXz - } - packedBootstrapTest { - java.srcDir('packaging/xz/src/test/java') - compileClasspath += sourceSets.packedBootstrap.output + sourceSets.packedBootstrap.compileClasspath - runtimeClasspath += sourceSets.packedBootstrap.output + sourceSets.packedBootstrap.compileClasspath - } -} - -dependencies { - packedBootstrapTestImplementation('junit:junit:4.13.2') -} - -tasks.named('compilePackedBootstrapJava', JavaCompile).configure { - dependsOn(originalJar) -} - -tasks.register('packedBootstrapTest', Test) { - group = 'verification' - testClassesDirs = sourceSets.packedBootstrapTest.output.classesDirs - classpath = sourceSets.packedBootstrapTest.runtimeClasspath -} - -tasks.register('packedJar', PackedJar) { - group = 'build' - description = 'Builds a Bukkit jar with an embedded XZ runtime payload.' - dependsOn(tasks.named('verifyBukkitArtifact'), tasks.named('packedBootstrapClasses'), configurations.nativeRuntime) - inputJar.set(originalJar.flatMap { it.archiveFile }) - bootstrapClasses.from(sourceSets.packedBootstrap.output.classesDirs) - decoderJars.from(configurations.packedXz) - nativeArtifacts.set(providers.provider { - Map resolved = new LinkedHashMap<>() - configurations.nativeRuntime.incoming.artifacts.artifacts.each { artifact -> - resolved.put(artifact.id.componentIdentifier, artifact.file) - } - PackedJar.resolveNativeArtifacts(resolved) - }) - outputJar.set(layout.buildDirectory.file(originalJar.flatMap { it.archiveFileName } - .map { String name -> "libs/${name.replaceFirst(/\.jar$/, '-xz.jar')}" })) -} - -tasks.named('check').configure { - dependsOn(tasks.named('packedBootstrapTest')) -} diff --git a/packaging/xz/src/main/java/art/arcane/iris/platform/bootstrap/PackedRuntime.java b/packaging/xz/src/main/java/art/arcane/iris/platform/bootstrap/PackedRuntime.java deleted file mode 100644 index 2aea88f42..000000000 --- a/packaging/xz/src/main/java/art/arcane/iris/platform/bootstrap/PackedRuntime.java +++ /dev/null @@ -1,213 +0,0 @@ -package art.arcane.iris.platform.bootstrap; - -import art.arcane.iris.util.slimjar.injector.loader.factory.InjectableFactory; -import org.tukaani.xz.XZInputStream; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HexFormat; -import java.util.List; -import java.util.jar.JarFile; -import java.util.logging.Logger; - -public final class PackedRuntime { - private static final long MAXIMUM_BYTES = 256L * 1024 * 1024; - private static final int DECODER_MEMORY_KIB = 128 * 1024; - private static Path runtime; - private static RuntimeArchive archive; - private static Field archiveField; - - private PackedRuntime() { - } - - public static synchronized void install() { - if (runtime != null) { - return; - } - try { - Path pluginJar = Path.of(PackedRuntime.class.getProtectionDomain().getCodeSource().getLocation().toURI()); - Path cache = pluginJar.getParent().resolve("Iris/cache/runtime"); - String hash; - try (InputStream input = resource("runtime.sha256")) { - hash = new String(input.readNBytes(128), StandardCharsets.US_ASCII).strip(); - } - if (!hash.matches("[0-9a-f]{64}")) { - throw new IOException("Invalid embedded runtime checksum"); - } - Path payload = cache.resolve(hash + ".jar"); - if (Files.exists(payload)) { - verify(payload, hash); - } else { - try (InputStream input = resource("runtime.jar.xz")) { - extract(input, payload, hash); - } - } - InjectableFactory.UNSAFE.create(cache, List.of(), PackedRuntime.class.getClassLoader()) - .inject(payload.toUri().toURL()); - ClassLoader loader = PackedRuntime.class.getClassLoader(); - archiveField = archiveOwner(loader).getDeclaredField("jar"); - archive = replaceArchive(loader, archiveField, payload, !temporaryLoader(loader)); - runtime = payload; - Logger.getLogger("Iris").info("Loaded embedded XZ runtime: " + payload.getFileName()); - } catch (IOException | URISyntaxException | ReflectiveOperationException failure) { - throw new IllegalStateException("Unable to load the embedded Iris runtime", failure); - } - } - - public static synchronized void releaseTemporary(Throwable failure) { - ClassLoader loader = PackedRuntime.class.getClassLoader(); - if (!temporaryLoader(loader) || archive == null) { - return; - } - RuntimeArchive released = archive; - archive = null; - try (released) { - archiveField.set(loader, released.original); - } catch (IOException | IllegalAccessException cleanupFailure) { - if (failure != null) { - failure.addSuppressed(cleanupFailure); - } else { - throw new IllegalStateException("Unable to close the Iris runtime loader archive", cleanupFailure); - } - } - } - - public static File runtimeJar() { - install(); - return runtime.toFile(); - } - - static RuntimeArchive replaceArchive(ClassLoader loader, Field field, Path payload, boolean closeOriginal) - throws IOException, IllegalAccessException { - if (field.getType() != JarFile.class || !field.getDeclaringClass().isInstance(loader)) { - throw new IllegalArgumentException("Unsupported plugin archive field: " + field); - } - field.setAccessible(true); - JarFile original = (JarFile) field.get(loader); - RuntimeArchive replacement = new RuntimeArchive(new ArchiveOptions(payload, original, closeOriginal)); - try { - field.set(loader, replacement); - } catch (IllegalAccessException | RuntimeException failure) { - try { - replacement.closePayload(); - } catch (IOException closeFailure) { - failure.addSuppressed(closeFailure); - } - throw failure; - } - return replacement; - } - - static void extract(InputStream compressed, Path target, String expectedHash) throws IOException { - Files.createDirectories(target.getParent()); - Path temporary = Files.createTempFile(target.getParent(), ".runtime-", ".jar"); - try { - MessageDigest digest = digest(); - try (InputStream input = new DigestInputStream(new XZInputStream(compressed, DECODER_MEMORY_KIB), digest); - OutputStream output = Files.newOutputStream(temporary)) { - byte[] buffer = new byte[65536]; - long size = 0; - int count; - while ((count = input.read(buffer)) != -1) { - size += count; - if (size > MAXIMUM_BYTES) { - throw new IOException("Embedded Iris runtime exceeds the size limit"); - } - output.write(buffer, 0, count); - } - } - requireHash(digest.digest(), expectedHash); - Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); - } finally { - Files.deleteIfExists(temporary); - } - } - - static void verify(Path payload, String expectedHash) throws IOException { - if (Files.size(payload) > MAXIMUM_BYTES) { - throw new IOException("Embedded Iris runtime exceeds the size limit"); - } - MessageDigest digest = digest(); - try (InputStream input = new DigestInputStream(Files.newInputStream(payload), digest)) { - input.transferTo(OutputStream.nullOutputStream()); - } - requireHash(digest.digest(), expectedHash); - } - - private static InputStream resource(String name) throws IOException { - InputStream input = PackedRuntime.class.getResourceAsStream("/META-INF/iris/" + name); - if (input == null) { - throw new IOException("Missing embedded Iris runtime resource: " + name); - } - return input; - } - - private static Class archiveOwner(ClassLoader loader) { - for (Class type = loader.getClass(); type != null; type = type.getSuperclass()) { - if (type.getName().equals("org.bukkit.plugin.java.PluginClassLoader") - || type.getName().equals("io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader")) { - return type; - } - } - throw new IllegalStateException("Unsupported Iris plugin classloader: " + loader.getClass().getName()); - } - - private static boolean temporaryLoader(ClassLoader loader) { - return loader.getClass().getName() - .equals("io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader"); - } - - private static MessageDigest digest() { - try { - return MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException failure) { - throw new IllegalStateException("SHA-256 is unavailable", failure); - } - } - - private static void requireHash(byte[] actual, String expected) throws IOException { - if (!HexFormat.of().formatHex(actual).equals(expected)) { - throw new IOException("Embedded Iris runtime checksum does not match"); - } - } - - static final class RuntimeArchive extends JarFile { - private final JarFile original; - private final boolean closeOriginal; - - RuntimeArchive(ArchiveOptions options) throws IOException { - super(options.payload().toFile()); - this.original = options.original(); - this.closeOriginal = options.closeOriginal(); - } - - @Override - public void close() throws IOException { - if (closeOriginal) { - try (original) { - super.close(); - } - } else { - super.close(); - } - } - - private void closePayload() throws IOException { - super.close(); - } - } - - private record ArchiveOptions(Path payload, JarFile original, boolean closeOriginal) { - } -} diff --git a/packaging/xz/src/test/java/art/arcane/iris/platform/bootstrap/PackedRuntimeTest.java b/packaging/xz/src/test/java/art/arcane/iris/platform/bootstrap/PackedRuntimeTest.java deleted file mode 100644 index 295e8626f..000000000 --- a/packaging/xz/src/test/java/art/arcane/iris/platform/bootstrap/PackedRuntimeTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package art.arcane.iris.platform.bootstrap; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.tukaani.xz.LZMA2Options; -import org.tukaani.xz.XZOutputStream; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.HexFormat; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.stream.Stream; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertSame; - -public class PackedRuntimeTest { - @Rule - public TemporaryFolder directory = new TemporaryFolder(); - - @Test - public void extractsAndVerifiesTheExactPayload() throws Exception { - byte[] content = "Iris runtime contents".getBytes(StandardCharsets.UTF_8); - String hash = hash(content); - Path target = directory.getRoot().toPath().resolve("runtime/" + hash + ".jar"); - PackedRuntime.extract(new ByteArrayInputStream(compress(content)), target, hash); - PackedRuntime.verify(target, hash); - assertArrayEquals(content, Files.readAllBytes(target)); - } - - @Test - public void rejectsAnIncorrectHashWithoutPublishingOrLeavingTemporaryFiles() throws Exception { - byte[] content = "Iris runtime contents".getBytes(StandardCharsets.UTF_8); - Path target = directory.getRoot().toPath().resolve("runtime/runtime.jar"); - assertThrows(IOException.class, () -> PackedRuntime.extract( - new ByteArrayInputStream(compress(content)), target, "0".repeat(64))); - assertFalse(Files.exists(target)); - try (Stream files = Files.list(target.getParent())) { - assertEquals(0L, files.count()); - } - } - - @Test - public void rejectsTruncatedCompressedData() throws Exception { - byte[] content = "Iris runtime contents".getBytes(StandardCharsets.UTF_8); - byte[] compressed = compress(content); - Path target = directory.getRoot().toPath().resolve("runtime/runtime.jar"); - assertThrows(IOException.class, () -> PackedRuntime.extract( - new ByteArrayInputStream(compressed, 0, compressed.length / 2), target, hash(content))); - assertFalse(Files.exists(target)); - } - - @Test - public void detectsModifiedCachedRuntime() throws Exception { - byte[] content = "Iris runtime contents".getBytes(StandardCharsets.UTF_8); - Path target = directory.newFile("runtime.jar").toPath(); - Files.writeString(target, "changed"); - assertThrows(IOException.class, () -> PackedRuntime.verify(target, hash(content))); - } - - @Test - public void nativeArchiveReplacementKeepsClassIdentityAndClosesOwnedHandles() throws Exception { - Path outer = createArchive("outer.jar", false); - Path payload = createArchive("payload.jar", true); - JarFile original = new JarFile(outer.toFile()); - PackedRuntime.RuntimeArchive replacement; - try (ArchiveLoader loader = new ArchiveLoader(original)) { - assertThrows(ClassNotFoundException.class, () -> loader.loadClass(Fixture.class.getName())); - replacement = PackedRuntime.replaceArchive(loader, ArchiveLoader.class.getDeclaredField("jar"), payload, true); - Class loaded = loader.loadClass(Fixture.class.getName()); - assertSame(loader, loaded.getClassLoader()); - assertEquals("runtime", loaded.getMethod("value").invoke(null)); - assertEquals(1, loader.definitions); - } - assertThrows(IllegalStateException.class, original::size); - assertThrows(IllegalStateException.class, replacement::size); - } - - @Test - public void temporaryArchiveCloseLeavesTheSharedOriginalOpen() throws Exception { - Path outer = createArchive("outer.jar", false); - Path payload = createArchive("payload.jar", true); - try (JarFile original = new JarFile(outer.toFile()); - ArchiveLoader loader = new ArchiveLoader(original)) { - PackedRuntime.RuntimeArchive replacement = PackedRuntime.replaceArchive( - loader, ArchiveLoader.class.getDeclaredField("jar"), payload, false); - replacement.close(); - assertEquals(0, original.size()); - assertThrows(IllegalStateException.class, replacement::size); - } - } - - private Path createArchive(String name, boolean withFixture) throws IOException { - Path path = directory.getRoot().toPath().resolve(name); - try (JarOutputStream output = new JarOutputStream(Files.newOutputStream(path))) { - if (withFixture) { - String entry = Fixture.class.getName().replace('.', '/') + ".class"; - output.putNextEntry(new JarEntry(entry)); - try (InputStream bytes = getClass().getClassLoader().getResourceAsStream(entry)) { - bytes.transferTo(output); - } - output.closeEntry(); - } - } - return path; - } - - private static byte[] compress(byte[] content) throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - try (XZOutputStream output = new XZOutputStream(bytes, new LZMA2Options(1))) { - output.write(content); - } - return bytes.toByteArray(); - } - - private static String hash(byte[] content) throws Exception { - return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(content)); - } - - public static class Fixture { - public static String value() { - return "runtime"; - } - } - - private static final class ArchiveLoader extends URLClassLoader { - private final JarFile jar; - private int definitions; - - private ArchiveLoader(JarFile jar) { - super(new URL[0], ClassLoader.getPlatformClassLoader()); - this.jar = jar; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - JarEntry entry = jar.getJarEntry(name.replace('.', '/') + ".class"); - if (entry == null) { - throw new ClassNotFoundException(name); - } - try (InputStream input = jar.getInputStream(entry)) { - byte[] bytes = input.readAllBytes(); - definitions++; - return defineClass(name, bytes, 0, bytes.length); - } catch (IOException failure) { - throw new ClassNotFoundException(name, failure); - } - } - - @Override - public void close() throws IOException { - try (jar) { - super.close(); - } - } - } -}