This commit is contained in:
Brian Neumann-Fopiano
2026-07-10 04:05:24 -04:00
parent 6111b5670a
commit 402bcb04fe
112 changed files with 5610 additions and 733 deletions
+5
View File
@@ -16,6 +16,7 @@ String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
dependencies {
implementation(project(':core'))
compileOnly(libs.spigot)
testImplementation('junit:junit:4.13.2')
compileOnly(volmLibCoordinate) {
transitive = false
}
@@ -44,6 +45,10 @@ tasks.named('run', JavaExec).configure {
dependsOn(':core:compileJava')
}
tasks.named('test').configure {
dependsOn(':core:compileJava')
}
String probePack = providers.gradleProperty('probePack')
.orElse('/Users/brianfopiano/Developer/RemoteGit/[Minecraft Server]/consumers/plugin-consumers/instances/purpur-26.2/plugins/Iris/packs/overworld')
.get()
@@ -18,15 +18,23 @@
package art.arcane.iris.probe;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;
public final class ClassloadProbe {
private static final String ALLOWLIST_RESOURCE = "/classload-allowlist.tsv";
private static final String[] CRITICAL_PREFIXES = {
"art.arcane.iris.engine.",
"art.arcane.iris.util.",
@@ -34,26 +42,6 @@ public final class ClassloadProbe {
"art.arcane.iris.spi.",
};
private static final String[] EXEMPT_PREFIXES = {
"art.arcane.iris.platform.bukkit.",
"art.arcane.iris.engine.platform.",
"art.arcane.iris.util.common.plugin.",
"art.arcane.iris.util.common.inventorygui.",
"art.arcane.iris.util.common.director.",
"art.arcane.iris.util.common.data.registry.",
};
private static final String[] EXEMPT_CLASSES = {
"art.arcane.iris.util.common.misc.Bindings",
"art.arcane.iris.util.common.misc.SlimJar",
"art.arcane.iris.util.common.misc.ServerProperties",
"art.arcane.iris.util.common.data.IrisCustomData",
"art.arcane.iris.engine.IrisWorldManager",
"art.arcane.iris.engine.framework.EngineAssignedWorldManager",
"art.arcane.iris.engine.object.StudioMode",
"art.arcane.iris.engine.framework.placer.WorldObjectPlacer",
};
public static void main(String[] args) throws IOException {
art.arcane.iris.spi.IrisPlatforms.bind(new StubPlatform());
boolean bukkitPresent = true;
@@ -63,39 +51,42 @@ public final class ClassloadProbe {
bukkitPresent = false;
}
System.out.println("[probe] org.bukkit on classpath: " + bukkitPresent);
Path classesRoot = Path.of(args[0]);
List<String> names = new ArrayList<>();
try (Stream<Path> walk = Files.walk(classesRoot)) {
walk.filter(p -> p.toString().endsWith(".class"))
.forEach(p -> {
String rel = classesRoot.relativize(p).toString();
String name = rel.substring(0, rel.length() - 6).replace('/', '.');
if (name.contains("$")) {
return;
}
names.add(name);
});
if (bukkitPresent) {
System.out.println("[probe] RESULT: FAIL - org.bukkit must be absent from the classload probe runtime");
System.exit(1);
return;
}
names.sort(String::compareTo);
TreeMap<String, String> criticalFailures = new TreeMap<>();
TreeMap<String, String> otherFailures = new TreeMap<>();
TreeMap<String, Allowance> allowlist = loadAllowlist();
Path classesRoot = Path.of(args[0]);
List<String> names = scanClassNames(classesRoot);
TreeMap<String, Failure> criticalFailures = new TreeMap<>();
TreeMap<String, Failure> otherFailures = new TreeMap<>();
int loaded = 0;
int nestedTotal = 0;
int nestedLoaded = 0;
int criticalTotal = 0;
for (String name : names) {
boolean critical = matchesAny(name, CRITICAL_PREFIXES) && !matchesAny(name, EXEMPT_PREFIXES) && !exactAny(name);
boolean nested = name.contains("$");
if (nested) {
nestedTotal++;
}
boolean critical = matchesAny(name, CRITICAL_PREFIXES) && !allowlist.containsKey(name);
if (critical) {
criticalTotal++;
}
try {
Class.forName(name, true, ClassloadProbe.class.getClassLoader());
loaded++;
if (nested) {
nestedLoaded++;
}
} catch (Throwable failure) {
if (System.getenv("PROBE_TRACE") != null && name.contains(System.getenv("PROBE_TRACE"))) {
failure.printStackTrace(System.out);
}
String cause = rootCause(failure);
Failure cause = Failure.from(failure);
if (critical) {
criticalFailures.put(name, cause);
} else {
@@ -105,25 +96,56 @@ public final class ClassloadProbe {
}
System.out.println("[probe] classes scanned: " + names.size() + ", initialized OK: " + loaded);
System.out.println("[probe] nested classes scanned: " + nestedTotal + ", initialized OK: " + nestedLoaded
+ ", initialization failures: " + (nestedTotal - nestedLoaded));
System.out.println("[probe] critical set: " + criticalTotal + ", critical failures: " + criticalFailures.size());
criticalFailures.forEach((name, cause) -> System.out.println(" CRITICAL " + name + " -> " + cause));
criticalFailures.forEach((name, cause) -> System.out.println(" CRITICAL " + name + " -> " + cause.display()));
System.out.println("[probe] non-critical failures: " + otherFailures.size());
otherFailures.forEach((name, cause) -> System.out.println(" other " + name + " -> " + cause));
Review review = review(otherFailures, allowlist);
System.out.println("[probe] reviewed allowances: " + allowlist.size() + ", matched failures: " + review.matched());
otherFailures.forEach((name, cause) -> {
Allowance allowance = allowlist.get(name);
String status = allowance != null && allowance.matches(cause) ? "allowed " + allowance.category() : "UNEXPECTED";
System.out.println(" " + status + " " + name + " -> " + cause.display());
});
System.out.println("[probe] unexpected non-critical failures: " + review.unexpected().size());
System.out.println("[probe] stale allowances: " + review.staleAllowances().size());
review.staleAllowances().forEach((name, allowance) -> System.out.println(" STALE " + name + " -> " + allowance));
if (!criticalFailures.isEmpty()) {
if (!criticalFailures.isEmpty() || !review.passes()) {
System.out.println("[probe] RESULT: FAIL");
System.exit(1);
}
System.out.println("[probe] RESULT: PASS");
}
private static boolean exactAny(String name) {
for (String exempt : EXEMPT_CLASSES) {
if (name.equals(exempt)) {
return true;
static Review review(Map<String, Failure> failures, Map<String, Allowance> allowlist) {
TreeMap<String, Failure> unexpected = new TreeMap<>();
TreeMap<String, Allowance> staleAllowances = new TreeMap<>(allowlist);
int matched = 0;
for (Map.Entry<String, Failure> entry : failures.entrySet()) {
Allowance allowance = allowlist.get(entry.getKey());
staleAllowances.remove(entry.getKey());
if (allowance == null || !allowance.matches(entry.getValue())) {
unexpected.put(entry.getKey(), entry.getValue());
} else {
matched++;
}
}
return false;
return new Review(matched, unexpected, staleAllowances);
}
static List<String> scanClassNames(Path classesRoot) throws IOException {
List<String> names = new ArrayList<>();
try (Stream<Path> walk = Files.walk(classesRoot)) {
walk.filter(path -> path.toString().endsWith(".class"))
.forEach(path -> {
String relative = classesRoot.relativize(path).toString();
names.add(relative.substring(0, relative.length() - 6).replace(File.separatorChar, '.'));
});
}
names.sort(String::compareTo);
return names;
}
private static boolean matchesAny(String name, String[] prefixes) {
@@ -135,11 +157,91 @@ public final class ClassloadProbe {
return false;
}
private static String rootCause(Throwable failure) {
Throwable cause = failure;
while (cause.getCause() != null && cause.getCause() != cause) {
cause = cause.getCause();
private static TreeMap<String, Allowance> loadAllowlist() throws IOException {
InputStream resource = ClassloadProbe.class.getResourceAsStream(ALLOWLIST_RESOURCE);
if (resource == null) {
throw new IOException("Classload allowlist resource not found: " + ALLOWLIST_RESOURCE);
}
TreeMap<String, Allowance> allowlist = new TreeMap<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource, StandardCharsets.UTF_8))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
if (line.isBlank()) {
continue;
}
String[] fields = line.split("\\t", -1);
if (fields.length != 3 || fields[0].isBlank() || fields[2].isBlank()) {
throw new IOException("Invalid classload allowlist entry at line " + lineNumber);
}
Allowance allowance;
try {
allowance = new Allowance(AllowanceCategory.valueOf(fields[1]), fields[2]);
} catch (IllegalArgumentException exception) {
throw new IOException("Invalid classload allowlist category at line " + lineNumber + ": " + fields[1], exception);
}
if (allowlist.put(fields[0], allowance) != null) {
throw new IOException("Duplicate classload allowlist class at line " + lineNumber + ": " + fields[0]);
}
}
}
return allowlist;
}
enum AllowanceCategory {
BUKKIT_API,
PAPERLIB_API,
ADVENTURE_API,
MYTHICMOBS_API,
SERVER_RUNTIME_FILE
}
record Allowance(AllowanceCategory category, String detail) {
boolean matches(Failure failure) {
return switch (category) {
case BUKKIT_API -> detail.startsWith("org.bukkit.") && failure.isMissingClassIn("org.bukkit.");
case PAPERLIB_API -> detail.startsWith("io.papermc.lib.") && failure.isMissingClassIn("io.papermc.lib.");
case ADVENTURE_API -> detail.startsWith("net.kyori.adventure.") && failure.isMissingClassIn("net.kyori.adventure.");
case MYTHICMOBS_API -> detail.startsWith("io.lumine.mythic.") && failure.isMissingClassIn("io.lumine.mythic.");
case SERVER_RUNTIME_FILE -> failure.exceptionClass().equals("java.io.FileNotFoundException")
&& failure.message().startsWith(detail);
};
}
}
record Failure(String exceptionClass, String message) {
static Failure from(Throwable failure) {
Throwable cause = failure;
while (cause.getCause() != null && cause.getCause() != cause) {
cause = cause.getCause();
}
return new Failure(cause.getClass().getName(), String.valueOf(cause.getMessage()));
}
boolean isMissingClassIn(String packagePrefix) {
if (exceptionClass.equals("java.lang.ClassNotFoundException")) {
return message.startsWith(packagePrefix);
}
String internalPrefix = packagePrefix.replace('.', '/');
if (exceptionClass.equals("java.lang.NoClassDefFoundError")) {
return message.startsWith(internalPrefix);
}
return exceptionClass.equals("java.lang.ExceptionInInitializerError")
&& message.contains("NoClassDefFoundError: " + internalPrefix);
}
String display() {
int separator = exceptionClass.lastIndexOf('.');
String simpleName = separator < 0 ? exceptionClass : exceptionClass.substring(separator + 1);
return simpleName + ": " + message;
}
}
record Review(int matched, TreeMap<String, Failure> unexpected, TreeMap<String, Allowance> staleAllowances) {
boolean passes() {
return unexpected.isEmpty() && staleAllowances.isEmpty();
}
return cause.getClass().getSimpleName() + ": " + cause.getMessage();
}
}
@@ -0,0 +1,83 @@
art.arcane.iris.core.IrisWorlds BUKKIT_API org.bukkit.command.CommandSender
art.arcane.iris.core.ServerConfigurator BUKKIT_API org.bukkit.command.CommandSender
art.arcane.iris.core.edit.BlockSignal BUKKIT_API org.bukkit.entity.Entity
art.arcane.iris.core.edit.BlockSignal$1 BUKKIT_API org.bukkit.entity.Entity
art.arcane.iris.core.edit.DustRevealer ADVENTURE_API net.kyori.adventure.text.format.TextColor
art.arcane.iris.core.events.IrisEngineEvent BUKKIT_API org.bukkit.event.Event
art.arcane.iris.core.events.IrisEngineHotloadEvent BUKKIT_API org.bukkit.event.Event
art.arcane.iris.core.events.IrisLootEvent BUKKIT_API org.bukkit.event.Event
art.arcane.iris.core.events.IrisLootEvent$1 BUKKIT_API org.bukkit.loot.LootTable
art.arcane.iris.core.events.IrisLootEvent$2 BUKKIT_API org.bukkit.inventory.InventoryHolder
art.arcane.iris.core.lifecycle.PaperLibBootstrap PAPERLIB_API io.papermc.lib.environments.Environment
art.arcane.iris.core.lifecycle.PaperLibBootstrap$ModernPaperEnvironment PAPERLIB_API io.papermc.lib.environments.PaperEnvironment
art.arcane.iris.core.link.ExternalDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.CraftEngineDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.EcoItemsDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.ExecutableItemsDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.HMCLeavesDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.ItemAdderDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.KGeneratorsDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.MMOItemsDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.MythicCrucibleDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.MythicMobsDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.link.data.MythicMobsDataProvider$IrisBiomeCondition MYTHICMOBS_API io.lumine.mythic.api.skills.conditions.ILocationCondition
art.arcane.iris.core.link.data.MythicMobsDataProvider$IrisRegionCondition MYTHICMOBS_API io.lumine.mythic.api.skills.conditions.ILocationCondition
art.arcane.iris.core.link.data.NexoDataProvider BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.nms.INMS BUKKIT_API org.bukkit.Bukkit
art.arcane.iris.core.project.IrisProject BUKKIT_API org.bukkit.entity.Entity
art.arcane.iris.core.runtime.WorldRuntimeControlService BUKKIT_API org.bukkit.entity.Entity
art.arcane.iris.core.service.BoardSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.BoardSVC$PlayerBoard BUKKIT_API org.bukkit.entity.Entity
art.arcane.iris.core.service.ExternalDataSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.GlobalCacheSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.LogFilterSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.ObjectSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.ObjectStudioSaveService BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.PreservationSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.StudioSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.service.TreeSVC BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.core.tools.IrisToolbelt BUKKIT_API org.bukkit.command.CommandSender
art.arcane.iris.core.tools.IrisWorldCreator BUKKIT_API org.bukkit.generator.ChunkGenerator
art.arcane.iris.core.tools.TreePlausibilizer BUKKIT_API org.bukkit.Keyed
art.arcane.iris.engine.IrisWorldManager BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.engine.framework.EngineAssignedWorldManager BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.engine.framework.placer.WorldObjectPlacer BUKKIT_API org.bukkit.event.Event
art.arcane.iris.engine.object.IrisDirection$11 BUKKIT_API org.bukkit.block.BlockFace
art.arcane.iris.engine.object.IrisEffect$BukkitFx BUKKIT_API org.bukkit.entity.Entity
art.arcane.iris.engine.object.IrisEntity$BukkitOps BUKKIT_API org.bukkit.loot.LootTable
art.arcane.iris.engine.object.IrisEntity$BukkitOps$1 BUKKIT_API org.bukkit.loot.LootTable
art.arcane.iris.engine.object.IrisObjectRotation$1 BUKKIT_API org.bukkit.block.BlockFace
art.arcane.iris.engine.object.IrisObjectRotation$Faces BUKKIT_API org.bukkit.block.BlockFace
art.arcane.iris.engine.object.LegacyTileData$BannerHandler BUKKIT_API org.bukkit.Keyed
art.arcane.iris.engine.object.LegacyTileData$Handlers BUKKIT_API org.bukkit.block.BlockState
art.arcane.iris.engine.object.LegacyTileData$SignHandler BUKKIT_API org.bukkit.Keyed
art.arcane.iris.engine.object.LegacyTileData$SignTags BUKKIT_API org.bukkit.Tag
art.arcane.iris.engine.object.LegacyTileData$SignTags$1 BUKKIT_API org.bukkit.Tag
art.arcane.iris.engine.platform.BukkitChunkGenerator BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.engine.platform.BukkitChunkGenerator$2 BUKKIT_API org.bukkit.HeightMap
art.arcane.iris.engine.platform.DummyBiomeProvider BUKKIT_API org.bukkit.generator.BiomeProvider
art.arcane.iris.engine.platform.DummyChunkGenerator BUKKIT_API org.bukkit.generator.ChunkGenerator
art.arcane.iris.engine.platform.EngineBukkitOps BUKKIT_API org.bukkit.entity.Entity
art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator BUKKIT_API org.bukkit.Material
art.arcane.iris.platform.bukkit.BukkitBlockResolution BUKKIT_API org.bukkit.Material
art.arcane.iris.platform.bukkit.BukkitPlatform BUKKIT_API org.bukkit.command.CommandSender
art.arcane.iris.util.common.data.IrisCustomData BUKKIT_API org.bukkit.block.data.BlockData
art.arcane.iris.util.common.data.registry.Attributes BUKKIT_API org.bukkit.attribute.Attribute
art.arcane.iris.util.common.data.registry.Materials BUKKIT_API org.bukkit.Material
art.arcane.iris.util.common.data.registry.Particles BUKKIT_API org.bukkit.Particle
art.arcane.iris.util.common.director.handlers.VectorHandler BUKKIT_API org.bukkit.util.Vector
art.arcane.iris.util.common.format.C$DyeMaps BUKKIT_API org.bukkit.DyeColor
art.arcane.iris.util.common.inventorygui.WindowResolution BUKKIT_API org.bukkit.event.inventory.InventoryType
art.arcane.iris.util.common.misc.Bindings BUKKIT_API org.bukkit.plugin.Plugin
art.arcane.iris.util.common.misc.ServerProperties SERVER_RUNTIME_FILE server.properties
art.arcane.iris.util.common.misc.SlimJar BUKKIT_API org.bukkit.plugin.Plugin
art.arcane.iris.util.common.plugin.CommandDummy BUKKIT_API org.bukkit.command.CommandSender
art.arcane.iris.util.common.plugin.Controller BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.util.common.plugin.IController BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.util.common.plugin.IrisService BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.util.common.plugin.RouterCommand BUKKIT_API org.bukkit.command.Command
art.arcane.iris.util.common.plugin.VolmitPlugin BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.util.common.plugin.VolmitSender BUKKIT_API org.bukkit.command.CommandSender
art.arcane.iris.util.common.plugin.chunk.ChunkTickets BUKKIT_API org.bukkit.event.Listener
art.arcane.iris.util.nbt.common.mca.NBTWorld$Holder BUKKIT_API org.bukkit.Material
art.arcane.iris.util.project.sentry.ServerID$Holder BUKKIT_API org.bukkit.Bukkit
1 art.arcane.iris.core.IrisWorlds BUKKIT_API org.bukkit.command.CommandSender
2 art.arcane.iris.core.ServerConfigurator BUKKIT_API org.bukkit.command.CommandSender
3 art.arcane.iris.core.edit.BlockSignal BUKKIT_API org.bukkit.entity.Entity
4 art.arcane.iris.core.edit.BlockSignal$1 BUKKIT_API org.bukkit.entity.Entity
5 art.arcane.iris.core.edit.DustRevealer ADVENTURE_API net.kyori.adventure.text.format.TextColor
6 art.arcane.iris.core.events.IrisEngineEvent BUKKIT_API org.bukkit.event.Event
7 art.arcane.iris.core.events.IrisEngineHotloadEvent BUKKIT_API org.bukkit.event.Event
8 art.arcane.iris.core.events.IrisLootEvent BUKKIT_API org.bukkit.event.Event
9 art.arcane.iris.core.events.IrisLootEvent$1 BUKKIT_API org.bukkit.loot.LootTable
10 art.arcane.iris.core.events.IrisLootEvent$2 BUKKIT_API org.bukkit.inventory.InventoryHolder
11 art.arcane.iris.core.lifecycle.PaperLibBootstrap PAPERLIB_API io.papermc.lib.environments.Environment
12 art.arcane.iris.core.lifecycle.PaperLibBootstrap$ModernPaperEnvironment PAPERLIB_API io.papermc.lib.environments.PaperEnvironment
13 art.arcane.iris.core.link.ExternalDataProvider BUKKIT_API org.bukkit.event.Listener
14 art.arcane.iris.core.link.data.CraftEngineDataProvider BUKKIT_API org.bukkit.event.Listener
15 art.arcane.iris.core.link.data.EcoItemsDataProvider BUKKIT_API org.bukkit.event.Listener
16 art.arcane.iris.core.link.data.ExecutableItemsDataProvider BUKKIT_API org.bukkit.event.Listener
17 art.arcane.iris.core.link.data.HMCLeavesDataProvider BUKKIT_API org.bukkit.event.Listener
18 art.arcane.iris.core.link.data.ItemAdderDataProvider BUKKIT_API org.bukkit.event.Listener
19 art.arcane.iris.core.link.data.KGeneratorsDataProvider BUKKIT_API org.bukkit.event.Listener
20 art.arcane.iris.core.link.data.MMOItemsDataProvider BUKKIT_API org.bukkit.event.Listener
21 art.arcane.iris.core.link.data.MythicCrucibleDataProvider BUKKIT_API org.bukkit.event.Listener
22 art.arcane.iris.core.link.data.MythicMobsDataProvider BUKKIT_API org.bukkit.event.Listener
23 art.arcane.iris.core.link.data.MythicMobsDataProvider$IrisBiomeCondition MYTHICMOBS_API io.lumine.mythic.api.skills.conditions.ILocationCondition
24 art.arcane.iris.core.link.data.MythicMobsDataProvider$IrisRegionCondition MYTHICMOBS_API io.lumine.mythic.api.skills.conditions.ILocationCondition
25 art.arcane.iris.core.link.data.NexoDataProvider BUKKIT_API org.bukkit.event.Listener
26 art.arcane.iris.core.nms.INMS BUKKIT_API org.bukkit.Bukkit
27 art.arcane.iris.core.project.IrisProject BUKKIT_API org.bukkit.entity.Entity
28 art.arcane.iris.core.runtime.WorldRuntimeControlService BUKKIT_API org.bukkit.entity.Entity
29 art.arcane.iris.core.service.BoardSVC BUKKIT_API org.bukkit.event.Listener
30 art.arcane.iris.core.service.BoardSVC$PlayerBoard BUKKIT_API org.bukkit.entity.Entity
31 art.arcane.iris.core.service.ExternalDataSVC BUKKIT_API org.bukkit.event.Listener
32 art.arcane.iris.core.service.GlobalCacheSVC BUKKIT_API org.bukkit.event.Listener
33 art.arcane.iris.core.service.LogFilterSVC BUKKIT_API org.bukkit.event.Listener
34 art.arcane.iris.core.service.ObjectSVC BUKKIT_API org.bukkit.event.Listener
35 art.arcane.iris.core.service.ObjectStudioSaveService BUKKIT_API org.bukkit.event.Listener
36 art.arcane.iris.core.service.PreservationSVC BUKKIT_API org.bukkit.event.Listener
37 art.arcane.iris.core.service.StudioSVC BUKKIT_API org.bukkit.event.Listener
38 art.arcane.iris.core.service.TreeSVC BUKKIT_API org.bukkit.event.Listener
39 art.arcane.iris.core.tools.IrisToolbelt BUKKIT_API org.bukkit.command.CommandSender
40 art.arcane.iris.core.tools.IrisWorldCreator BUKKIT_API org.bukkit.generator.ChunkGenerator
41 art.arcane.iris.core.tools.TreePlausibilizer BUKKIT_API org.bukkit.Keyed
42 art.arcane.iris.engine.IrisWorldManager BUKKIT_API org.bukkit.event.Listener
43 art.arcane.iris.engine.framework.EngineAssignedWorldManager BUKKIT_API org.bukkit.event.Listener
44 art.arcane.iris.engine.framework.placer.WorldObjectPlacer BUKKIT_API org.bukkit.event.Event
45 art.arcane.iris.engine.object.IrisDirection$11 BUKKIT_API org.bukkit.block.BlockFace
46 art.arcane.iris.engine.object.IrisEffect$BukkitFx BUKKIT_API org.bukkit.entity.Entity
47 art.arcane.iris.engine.object.IrisEntity$BukkitOps BUKKIT_API org.bukkit.loot.LootTable
48 art.arcane.iris.engine.object.IrisEntity$BukkitOps$1 BUKKIT_API org.bukkit.loot.LootTable
49 art.arcane.iris.engine.object.IrisObjectRotation$1 BUKKIT_API org.bukkit.block.BlockFace
50 art.arcane.iris.engine.object.IrisObjectRotation$Faces BUKKIT_API org.bukkit.block.BlockFace
51 art.arcane.iris.engine.object.LegacyTileData$BannerHandler BUKKIT_API org.bukkit.Keyed
52 art.arcane.iris.engine.object.LegacyTileData$Handlers BUKKIT_API org.bukkit.block.BlockState
53 art.arcane.iris.engine.object.LegacyTileData$SignHandler BUKKIT_API org.bukkit.Keyed
54 art.arcane.iris.engine.object.LegacyTileData$SignTags BUKKIT_API org.bukkit.Tag
55 art.arcane.iris.engine.object.LegacyTileData$SignTags$1 BUKKIT_API org.bukkit.Tag
56 art.arcane.iris.engine.platform.BukkitChunkGenerator BUKKIT_API org.bukkit.event.Listener
57 art.arcane.iris.engine.platform.BukkitChunkGenerator$2 BUKKIT_API org.bukkit.HeightMap
58 art.arcane.iris.engine.platform.DummyBiomeProvider BUKKIT_API org.bukkit.generator.BiomeProvider
59 art.arcane.iris.engine.platform.DummyChunkGenerator BUKKIT_API org.bukkit.generator.ChunkGenerator
60 art.arcane.iris.engine.platform.EngineBukkitOps BUKKIT_API org.bukkit.entity.Entity
61 art.arcane.iris.engine.platform.studio.generators.ObjectStudioGenerator BUKKIT_API org.bukkit.Material
62 art.arcane.iris.platform.bukkit.BukkitBlockResolution BUKKIT_API org.bukkit.Material
63 art.arcane.iris.platform.bukkit.BukkitPlatform BUKKIT_API org.bukkit.command.CommandSender
64 art.arcane.iris.util.common.data.IrisCustomData BUKKIT_API org.bukkit.block.data.BlockData
65 art.arcane.iris.util.common.data.registry.Attributes BUKKIT_API org.bukkit.attribute.Attribute
66 art.arcane.iris.util.common.data.registry.Materials BUKKIT_API org.bukkit.Material
67 art.arcane.iris.util.common.data.registry.Particles BUKKIT_API org.bukkit.Particle
68 art.arcane.iris.util.common.director.handlers.VectorHandler BUKKIT_API org.bukkit.util.Vector
69 art.arcane.iris.util.common.format.C$DyeMaps BUKKIT_API org.bukkit.DyeColor
70 art.arcane.iris.util.common.inventorygui.WindowResolution BUKKIT_API org.bukkit.event.inventory.InventoryType
71 art.arcane.iris.util.common.misc.Bindings BUKKIT_API org.bukkit.plugin.Plugin
72 art.arcane.iris.util.common.misc.ServerProperties SERVER_RUNTIME_FILE server.properties
73 art.arcane.iris.util.common.misc.SlimJar BUKKIT_API org.bukkit.plugin.Plugin
74 art.arcane.iris.util.common.plugin.CommandDummy BUKKIT_API org.bukkit.command.CommandSender
75 art.arcane.iris.util.common.plugin.Controller BUKKIT_API org.bukkit.event.Listener
76 art.arcane.iris.util.common.plugin.IController BUKKIT_API org.bukkit.event.Listener
77 art.arcane.iris.util.common.plugin.IrisService BUKKIT_API org.bukkit.event.Listener
78 art.arcane.iris.util.common.plugin.RouterCommand BUKKIT_API org.bukkit.command.Command
79 art.arcane.iris.util.common.plugin.VolmitPlugin BUKKIT_API org.bukkit.event.Listener
80 art.arcane.iris.util.common.plugin.VolmitSender BUKKIT_API org.bukkit.command.CommandSender
81 art.arcane.iris.util.common.plugin.chunk.ChunkTickets BUKKIT_API org.bukkit.event.Listener
82 art.arcane.iris.util.nbt.common.mca.NBTWorld$Holder BUKKIT_API org.bukkit.Material
83 art.arcane.iris.util.project.sentry.ServerID$Holder BUKKIT_API org.bukkit.Bukkit
@@ -0,0 +1,204 @@
package art.arcane.iris.probe;
import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class ClassloadProbeTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void scansNamedAndGeneratedNestedClasses() throws Exception {
Path classesRoot = temporaryFolder.newFolder("classes").toPath();
Path packageRoot = Files.createDirectories(classesRoot.resolve("art/arcane/iris"));
Files.createFile(packageRoot.resolve("Outer.class"));
Files.createFile(packageRoot.resolve("Outer$Nested.class"));
Files.createFile(packageRoot.resolve("Outer$1.class"));
Files.createFile(packageRoot.resolve("ignored.txt"));
List<String> names = ClassloadProbe.scanClassNames(classesRoot);
assertEquals(List.of(
"art.arcane.iris.Outer",
"art.arcane.iris.Outer$1",
"art.arcane.iris.Outer$Nested"
), names);
}
@Test
public void scansEmptyClassDirectory() throws Exception {
Path classesRoot = temporaryFolder.newFolder("empty-classes").toPath();
assertTrue(ClassloadProbe.scanClassNames(classesRoot).isEmpty());
}
@Test
public void acceptsMatchingReviewedFailure() {
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
"java.lang.ClassNotFoundException",
"org.bukkit.event.Listener"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.event.Listener"
);
ClassloadProbe.Review review = ClassloadProbe.review(
Map.of("art.arcane.iris.Allowed", failure),
Map.of("art.arcane.iris.Allowed", allowance)
);
assertTrue(review.passes());
}
@Test
public void rejectsUnlistedFailure() {
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
"java.lang.ClassNotFoundException",
"org.bukkit.event.Listener"
);
ClassloadProbe.Review review = ClassloadProbe.review(
Map.of("art.arcane.iris.Unlisted", failure),
Map.of()
);
assertFalse(review.passes());
assertTrue(review.unexpected().containsKey("art.arcane.iris.Unlisted"));
}
@Test
public void acceptsDifferentMissingClassInReviewedNamespace() {
ClassloadProbe.Failure changedFailure = new ClassloadProbe.Failure(
"java.lang.ClassNotFoundException",
"org.bukkit.Material"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.event.Listener"
);
ClassloadProbe.Review review = ClassloadProbe.review(
Map.of("art.arcane.iris.Changed", changedFailure),
Map.of("art.arcane.iris.Changed", allowance)
);
assertTrue(review.passes());
}
@Test
public void rejectsChangedFailureNamespace() {
ClassloadProbe.Failure changedFailure = new ClassloadProbe.Failure(
"java.lang.ClassNotFoundException",
"io.papermc.lib.environments.Environment"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.event.Listener"
);
ClassloadProbe.Review review = ClassloadProbe.review(
Map.of("art.arcane.iris.Changed", changedFailure),
Map.of("art.arcane.iris.Changed", allowance)
);
assertFalse(review.passes());
assertTrue(review.unexpected().containsKey("art.arcane.iris.Changed"));
}
@Test
public void rejectsNonMissingClassFailure() {
ClassloadProbe.Failure changedFailure = new ClassloadProbe.Failure(
"java.lang.IllegalStateException",
"org.bukkit.event.Listener"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.event.Listener"
);
assertFalse(allowance.matches(changedFailure));
}
@Test
public void acceptsBukkitFailureCachedByClassInitialization() {
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
"java.lang.ExceptionInInitializerError",
"Exception java.lang.NoClassDefFoundError: org/bukkit/Material [in thread \"main\"]"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.Material"
);
assertTrue(allowance.matches(failure));
}
@Test
public void acceptsDifferentBukkitFailureCachedByClassInitialization() {
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
"java.lang.ExceptionInInitializerError",
"Exception java.lang.NoClassDefFoundError: org/bukkit/World [in thread \"main\"]"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.Material"
);
assertTrue(allowance.matches(failure));
}
@Test
public void rejectsDifferentNamespaceCachedByClassInitialization() {
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
"java.lang.ExceptionInInitializerError",
"Exception java.lang.NoClassDefFoundError: io/papermc/lib/environments/Environment [in thread \"main\"]"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.Material"
);
assertFalse(allowance.matches(failure));
}
@Test
public void acceptsMythicMobsApiNamespace() {
ClassloadProbe.Failure failure = new ClassloadProbe.Failure(
"java.lang.ClassNotFoundException",
"io.lumine.mythic.api.skills.conditions.ILocationCondition"
);
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.MYTHICMOBS_API,
"io.lumine.mythic.api.skills.conditions.ILocationCondition"
);
assertTrue(allowance.matches(failure));
}
@Test
public void rejectsStaleAllowance() {
ClassloadProbe.Allowance allowance = new ClassloadProbe.Allowance(
ClassloadProbe.AllowanceCategory.BUKKIT_API,
"org.bukkit.event.Listener"
);
TreeMap<String, ClassloadProbe.Allowance> allowlist = new TreeMap<>();
allowlist.put("art.arcane.iris.Stale", allowance);
ClassloadProbe.Review review = ClassloadProbe.review(Map.of(), allowlist);
assertFalse(review.passes());
assertTrue(review.staleAllowances().containsKey("art.arcane.iris.Stale"));
}
}