mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
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
This commit is contained in:
@@ -59,7 +59,7 @@ apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
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 minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||||
String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3'))
|
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'))
|
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT'))
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
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 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 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'))
|
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT'))
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ apply from: file('../../gradle/modded-generation-build-revision.gradle')
|
|||||||
|
|
||||||
Properties rootProperties = new Properties()
|
Properties rootProperties = new Properties()
|
||||||
file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) }
|
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 minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.2'))
|
||||||
String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.2.0.59'))
|
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'))
|
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate').getOrElse(rootProperties.getProperty('volmLibCoordinate', 'com.github.VolmitSoftware:VolmLib:master-SNAPSHOT'))
|
||||||
|
|||||||
+22
-6
@@ -1,3 +1,4 @@
|
|||||||
|
import art.arcane.volmit.packaging.PackedJar
|
||||||
import art.arcane.volmit.packaging.PackagingMode
|
import art.arcane.volmit.packaging.PackagingMode
|
||||||
import art.arcane.volmit.packaging.NativeDevelopmentTargets
|
import art.arcane.volmit.packaging.NativeDevelopmentTargets
|
||||||
import io.papermc.paperweight.userdev.PaperweightUser
|
import io.papermc.paperweight.userdev.PaperweightUser
|
||||||
@@ -63,7 +64,7 @@ interface IrisFileOperations {
|
|||||||
IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
IrisFileOperations irisFileOperations = objects.newInstance(IrisFileOperations)
|
||||||
|
|
||||||
group = 'art.arcane'
|
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') {
|
project(':core') {
|
||||||
group = 'art.arcane'
|
group = 'art.arcane'
|
||||||
@@ -255,8 +256,9 @@ tasks.named('jar', Jar).configure {
|
|||||||
|
|
||||||
tasks.register('iris', Copy) {
|
tasks.register('iris', Copy) {
|
||||||
group = 'iris'
|
group = 'iris'
|
||||||
dependsOn('jar')
|
dependsOn('packedJar')
|
||||||
from(layout.buildDirectory.file("libs/${bukkitArtifactName}"))
|
from({ tasks.named('packedJar').get().outputJar })
|
||||||
|
rename { String ignored -> bukkitArtifactName }
|
||||||
into(layout.buildDirectory.dir('consumer'))
|
into(layout.buildDirectory.dir('consumer'))
|
||||||
doFirst {
|
doFirst {
|
||||||
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(staleConsumerBukkitJars) }
|
irisFileOperations.fileSystemOperations.delete { DeleteSpec spec -> spec.delete(staleConsumerBukkitJars) }
|
||||||
@@ -294,8 +296,9 @@ tasks.register('buildSwiftSwamp') {
|
|||||||
|
|
||||||
tasks.register('buildBukkit', Copy) {
|
tasks.register('buildBukkit', Copy) {
|
||||||
group = 'iris'
|
group = 'iris'
|
||||||
dependsOn('jar')
|
dependsOn('packedJar')
|
||||||
from(layout.buildDirectory.file("libs/${bukkitArtifactName}"))
|
from({ tasks.named('packedJar').get().outputJar })
|
||||||
|
rename { String ignored -> bukkitArtifactName }
|
||||||
into(pluginOutDirectory)
|
into(pluginOutDirectory)
|
||||||
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}.jar")
|
File legacyOutJar = new File(pluginOutDirectory, "Iris-${project.version}+mc${minecraftVersion}.jar")
|
||||||
doFirst {
|
doFirst {
|
||||||
@@ -928,6 +931,8 @@ pluginPackaging {
|
|||||||
create('distribution') {
|
create('distribution') {
|
||||||
taskName = 'jar'
|
taskName = 'jar'
|
||||||
policyName = 'Iris Bukkit'
|
policyName = 'Iris Bukkit'
|
||||||
|
packed = true
|
||||||
|
packedJarAccessors = ['art/arcane/iris/platform/bukkit/plugin/VolmitPlugin#getJarFile']
|
||||||
stripDirectories = true
|
stripDirectories = true
|
||||||
stripLocalVariables = true
|
stripLocalVariables = true
|
||||||
releaseCompression = providers.gradleProperty('compactRelease')
|
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<ComponentIdentifier, File> resolved = new LinkedHashMap<>()
|
||||||
|
configurations.nativeRuntime.incoming.artifacts.artifacts.each { artifact ->
|
||||||
|
resolved.put(artifact.id.componentIdentifier, artifact.file)
|
||||||
|
}
|
||||||
|
PackedJar.resolveNativeArtifacts(resolved)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ tasks.named('test').configure {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(gradleApi())
|
implementation(gradleApi())
|
||||||
implementation('org.ow2.asm:asm:9.10.1')
|
implementation('org.ow2.asm:asm:9.10.1')
|
||||||
implementation('org.tukaani:xz:1.10')
|
|
||||||
implementation('org.apache.commons:commons-compress:1.28.0')
|
implementation('org.apache.commons:commons-compress:1.28.0')
|
||||||
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
|
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
|
||||||
testImplementation('junit:junit:4.13.2')
|
testImplementation('junit:junit:4.13.2')
|
||||||
|
|||||||
@@ -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<String, File> getNativeArtifacts();
|
|
||||||
|
|
||||||
@Input
|
|
||||||
public List<String> getNativeModules() {
|
|
||||||
return getNativeArtifacts().get().keySet().stream().sorted().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@InputFiles
|
|
||||||
@PathSensitive(PathSensitivity.NONE)
|
|
||||||
public Collection<File> getNativeProviderJars() {
|
|
||||||
return getNativeArtifacts().get().values();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OutputFile
|
|
||||||
public abstract RegularFileProperty getOutputJar();
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
public void pack() throws IOException, NoSuchAlgorithmException {
|
|
||||||
Map<String, byte[]> bootstrap = new TreeMap<>();
|
|
||||||
for (File directory : getBootstrapClasses()) {
|
|
||||||
try (Stream<Path> 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<String, byte[]> 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<String, byte[]> bootstrap,
|
|
||||||
Map<String, File> nativeArtifacts)
|
|
||||||
throws IOException, NoSuchAlgorithmException {
|
|
||||||
Map<String, byte[]> 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<String> 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<String, byte[]> outer = new TreeMap<>();
|
|
||||||
for (Map.Entry<String, byte[]> 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<String, File> resolveNativeArtifacts(Map<ComponentIdentifier, File> resolved) {
|
|
||||||
Map<String, File> artifacts = new TreeMap<>();
|
|
||||||
for (Map.Entry<ComponentIdentifier, File> 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<String, byte[]> contents, Map<String, File> 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<String> 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<String> 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<String, byte[]> readJar(Path path) throws IOException {
|
|
||||||
Map<String, byte[]> 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<String, byte[]> contents, boolean stored) throws IOException {
|
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
|
||||||
try (JarOutputStream jar = new JarOutputStream(bytes)) {
|
|
||||||
jar.setLevel(9);
|
|
||||||
for (Map.Entry<String, byte[]> 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<String> bootstrapClosure(Map<String, byte[]> contents) {
|
|
||||||
List<String> roots = List.of("art/arcane/iris/Iris", PLUGIN_BASE, PAPER_LOADER,
|
|
||||||
PAPER_BOOTSTRAP, "art/arcane/volmlib/integration/ReloadAware");
|
|
||||||
ArrayDeque<String> 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<String> 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("<clinit>")) {
|
|
||||||
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, "<clinit>", "()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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String, byte[]> 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<String> 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<String, File> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String, byte[]> 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<String> 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("<clinit>:" + PackedJar.RUNTIME + ".install", "<clinit>:existing",
|
|
||||||
"getJarFile:" + PackedJar.RUNTIME + ".runtimeJar"), instructions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void retainsEntrypointReferencesAndHierarchyWithoutRecursiveMemberReferences() {
|
|
||||||
Map<String, byte[]> 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<String> 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<String> 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("<clinit>") && 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, "<init>", "()V", null, null);
|
|
||||||
constructor.visitCode();
|
|
||||||
constructor.visitVarInsn(Opcodes.ALOAD, 0);
|
|
||||||
constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()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, "<clinit>", "()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<String, byte[]> classes;
|
|
||||||
|
|
||||||
private FixtureLoader(Map<String, byte[]> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,10 +6,28 @@ import java.io.IOException;
|
|||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class Installer {
|
public class Installer {
|
||||||
private static volatile Instrumentation instrumentation;
|
private static volatile Instrumentation instrumentation;
|
||||||
private static final Map<ClassLoader, Boolean> retainedClassLoaders = new IdentityHashMap<>();
|
private static final Map<ClassLoader, Boolean> retainedClassLoaders = new IdentityHashMap<>();
|
||||||
|
private static final Map<Object, AtomicBoolean> 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) {
|
public static synchronized void retainClassLoader(ClassLoader loader) {
|
||||||
ClassLoader requiredLoader = Objects.requireNonNull(loader, "Plugin class loader");
|
ClassLoader requiredLoader = Objects.requireNonNull(loader, "Plugin class loader");
|
||||||
|
|||||||
+49
@@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,9 @@ public class Agent {
|
|||||||
installer.getMethod("retainClassLoader", ClassLoader.class);
|
installer.getMethod("retainClassLoader", ClassLoader.class);
|
||||||
installer.getMethod("deferClassLoaderClose", ClassLoader.class);
|
installer.getMethod("deferClassLoaderClose", ClassLoader.class);
|
||||||
installer.getMethod("releaseClassLoader", 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) {
|
public static void retainClassLoader(ClassLoader loader) {
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ org.gradle.jvmargs=-Xmx3072m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF
|
|||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.configureondemand=false
|
org.gradle.configureondemand=false
|
||||||
|
|
||||||
irisVersion=4.1.1-26.2
|
irisVersion=4.1.2-26.2
|
||||||
minecraftVersion=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
|
# Bukkit plugin api-version: lowest supported Minecraft release line so one artifact loads on 26.1.2 and 26.2
|
||||||
apiVersion=26.1
|
apiVersion=26.1
|
||||||
|
|||||||
@@ -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<ComponentIdentifier, File> 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'))
|
|
||||||
}
|
|
||||||
@@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Path> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user