Files
Iris/buildSrc/src/test/java/ModdedArtifactVerifierTest.java
T
Brian Neumann-Fopiano 62062c4a95 Added 1.21.1 mod targets
- Build Fabric, Forge and NeoForge per target listed in gradle/mod-targets.json (1.21.1, 26.3)
- Add 1.21.1 adapter source overlays under adapters/*/src/versions/1.21.1
- Add verifyModdedMatrix and per-target Java runtime verification of loader jars
- Re-inject persistent dimensions from the levels-ready hook before the watchdog starts
- Wait for queued chunks and the final save before pregen completes
- Add the 1.21.1 data version and datapack supported_formats range
- Cascade regions whose sea or shore pool loses every biome
- Use Gson calls available on the 1.21.1 runtime
2026-10-16 10:25:19 -04:00

521 lines
25 KiB
Java

import org.gradle.api.GradleException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class ModdedArtifactVerifierTest {
private static final String METADATA = "fabric.mod.json";
private static final String CODEC_CLASS = "art/arcane/volmlib/util/mantle/io/Lz4IOWorkerCodecSupport.class";
private static final String HARDWARE_CLASS = "art/arcane/iris/platform/bootstrap/getHardware.class";
private static final String MIXIN_CONFIG = "volmlib.entity.mixins.json";
private static final String CLIENT_MIXIN_CONFIG = "volmlib.client.mixins.json";
private static final String NEOFORGE_METADATA = "META-INF/neoforge.mods.toml";
private static final List<String> REQUIRED_ENTRIES = List.of(METADATA, CODEC_CLASS, HARDWARE_CLASS);
private static final byte[] INTERNAL_CODEC = (
"net/jpountz/lz4/LZ4BlockInputStream net/jpountz/lz4/LZ4BlockOutputStream"
).getBytes(StandardCharsets.ISO_8859_1);
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void rejectsClassesNewerThanSelectedRuntimeIncludingNestedJars() throws Exception {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC, "example/NewRuntime", null, "java/lang/Object", null);
writer.visitEnd();
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put("example/NewRuntime.class", writer.toByteArray());
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verifyJavaRuntime(artifact, 21));
assertTrue(failure.getMessage().contains("requires Java 25"));
ModdedArtifactVerifier.verifyJavaRuntime(artifact, 25);
File nested = createArtifact(Map.of("META-INF/jars/library.jar", Files.readAllBytes(artifact.toPath())));
GradleException nestedFailure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verifyJavaRuntime(nested, 21));
assertTrue(nestedFailure.getMessage().contains("library.jar!/example/NewRuntime.class"));
}
@Test
public void acceptsMinecraftRuntimeReferences() throws Exception {
Map<String, byte[]> entries = validEntries();
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES);
}
@Test
public void rejectsBukkitDependencyBootstrapClassesAndNestedClasses() throws Exception {
for (String internalName : List.of("art/arcane/iris/platform/bootstrap/SlimJar",
"art/arcane/iris/platform/bootstrap/SlimJar$BootstrapLogger",
"art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries",
"art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries$Provider")) {
Map<String, byte[]> entries = validEntries();
entries.put(internalName + ".class", classExtending(internalName, "java/lang/Object"));
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("Bukkit dependency bootstrap code"));
assertTrue(failure.getMessage().contains(internalName));
}
}
@Test
public void rejectsDanglingBukkitDependencyBootstrapReferences() throws Exception {
for (String internalName : List.of("art/arcane/iris/platform/bootstrap/SlimJar",
"art/arcane/iris/platform/bootstrap/NativeRuntimeLibraries")) {
Map<String, byte[]> entries = validEntries();
String caller = "art/arcane/iris/modded/BootstrapCaller";
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, caller, null, "java/lang/Object", null);
MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
"dependencyType", "()Ljava/lang/Class;", null, null);
method.visitCode();
method.visitLdcInsn(Type.getObjectType(internalName));
method.visitInsn(Opcodes.ARETURN);
method.visitMaxs(1, 0);
method.visitEnd();
writer.visitEnd();
entries.put(caller + ".class", writer.toByteArray());
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains(caller));
assertTrue(failure.getMessage().contains(internalName));
}
}
@Test
public void rejectsBukkitDependencyBootstrapInsideJarJar() throws Exception {
Map<String, byte[]> entries = validEntries();
String internalName = "art/arcane/iris/platform/bootstrap/SlimJar";
entries.put("META-INF/jarjar/dependency-support.jar", createNestedArtifact(Map.of(
internalName + ".class", classExtending(internalName, "java/lang/Object"))));
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("Bukkit dependency bootstrap code"));
assertTrue(failure.getMessage().contains("dependency-support.jar!/" + internalName));
}
@Test
public void rejectsExecutableEntriesRegardlessOfCase() throws Exception {
for (String executable : List.of("bin/installer.exe", "native/windows/helper.EXE")) {
Map<String, byte[]> entries = validEntries();
entries.put(executable, new byte[]{1});
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("executable .exe entries"));
assertTrue(failure.getMessage().contains(executable));
}
}
@Test
public void rejectsExecutableInsideJarJar() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("META-INF/jarjar/native-support.jar", createNestedArtifact(Map.of(
"native/helper.exe", new byte[]{1})));
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("executable .exe entries"));
assertTrue(failure.getMessage().contains("native-support.jar!/native/helper.exe"));
}
@Test
public void acceptsBundledCompressionLibraries() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("native/windows-x86_64/jlibdeflate.dll", new byte[]{1});
entries.put("native/linux-x86_64/libjlibdeflate.so", new byte[]{1});
entries.put("native/osx-aarch64/libjlibdeflate.dylib", new byte[]{1});
entries.put("com/fulcrumgenomics/jlibdeflate/NativeLoader.class",
classExtending("com/fulcrumgenomics/jlibdeflate/NativeLoader", "java/lang/Object"));
ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES);
}
@Test
public void rejectsClientLanguagesWithoutPackMetadata() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("assets/irisworldgen/lang/en_us.json", "{}".getBytes(StandardCharsets.UTF_8));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("client language resources without pack.mcmeta"));
}
@Test
public void acceptsClientLanguagesWithPackMetadata() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("assets/irisworldgen/lang/en_us.json", "{}".getBytes(StandardCharsets.UTF_8));
entries.put("pack.mcmeta", ("{\"pack\":{\"description\":\"Iris\","
+ "\"min_format\":[88,0],\"max_format\":[107,1]}}")
.getBytes(StandardCharsets.UTF_8));
ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES);
}
@Test
public void rejectsRelocatedRuntimeReference() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/Test.class",
"art/arcane/iris/shadow/jpountz/lz4/LZ4BlockInputStream"
.getBytes(StandardCharsets.ISO_8859_1));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("Iris-private runtime references"));
}
@Test
public void rejectsBukkitRelocatedGsonReferences() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/Test.class", "art/arcane/iris/util/gson/Gson"
.getBytes(StandardCharsets.ISO_8859_1));
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(createArtifact(entries), REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("Iris-private runtime references"));
}
@Test
public void rejectsBundledMinecraftRuntimeLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("net/jpountz/lz4/LZ4BlockInputStream.class", new byte[]{0});
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries"));
}
@Test
public void rejectsNestedMinecraftRuntimeLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("META-INF/jars/lz4-java.jar", new byte[]{0});
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries"));
}
@Test
public void rejectsRenamedJarJarRuntimeLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("META-INF/jarjar/runtime-support.jar", createNestedArtifact(Map.of(
"com/sun/jna/Native.class", new byte[]{0}
)));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("duplicates Minecraft runtime libraries"));
}
@Test
public void rejectsArtifactWithoutPlatformMetadata() throws Exception {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put(CODEC_CLASS, INTERNAL_CODEC);
entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("is missing " + METADATA));
}
@Test
public void rejectsUnrelocatedAsm() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("org/objectweb/asm/ClassReader.class", new byte[]{0});
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("ships unrelocated ASM"));
}
@Test
public void rejectsNestedJarMissingFromMetadata() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("META-INF/jars/fabric-rendering-v1.jar", createNestedArtifact(Map.of(
"net/fabricmc/Placeholder.class", new byte[]{0}
)));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("does not declare"));
}
@Test
public void rejectsDeclaredNestedJarThatIsNotShipped() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put(METADATA, fabricMetadata(
"[ { \"file\": \"META-INF/jars/fabric-api-base.jar\" } ]",
"[ \"" + MIXIN_CONFIG + "\" ]"));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("does not ship"));
}
@Test
public void acceptsNestedJarDeclaredInMetadata() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put(METADATA, fabricMetadata(
"[ { \"file\": \"META-INF/jars/fabric-api-base.jar\" } ]",
"[ \"" + MIXIN_CONFIG + "\" ]"));
entries.put("META-INF/jars/fabric-api-base.jar", createNestedArtifact(Map.of(
"net/fabricmc/Placeholder.class", new byte[]{0}
)));
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES);
}
@Test
public void rejectsMixinCompatibilityLevelAboveJava21() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put(MIXIN_CONFIG, mixinConfig("JAVA_25"));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25"));
}
@Test
public void rejectsMixinCompatibilityLevelAboveJava21RegisteredByManifest() throws Exception {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put("META-INF/mods.toml", "modId = \"irisworldgen\"".getBytes(StandardCharsets.UTF_8));
entries.put(CODEC_CLASS, INTERNAL_CODEC);
entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1));
entries.put(CLIENT_MIXIN_CONFIG, mixinConfig("JAVA_25"));
File artifact = createArtifact(entries, manifestWithMixinConfigs(CLIENT_MIXIN_CONFIG));
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, List.of(CODEC_CLASS, HARDWARE_CLASS)));
assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25"));
}
@Test
public void rejectsMixinCompatibilityLevelAboveJava21RegisteredByNeoforgeToml() throws Exception {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put(NEOFORGE_METADATA, ("[[mixins]]\nconfig = \"" + CLIENT_MIXIN_CONFIG + "\"\n")
.getBytes(StandardCharsets.UTF_8));
entries.put(CODEC_CLASS, INTERNAL_CODEC);
entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1));
entries.put(CLIENT_MIXIN_CONFIG, mixinConfig("JAVA_25"));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, List.of(CODEC_CLASS, HARDWARE_CLASS)));
assertTrue(failure.getMessage().contains("compatibilityLevel JAVA_25"));
}
@Test
public void rejectsRegisteredMixinConfigThatIsNotShipped() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.remove(MIXIN_CONFIG);
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("which is not in the jar"));
}
@Test
public void rejectsArtifactWithoutMixinRegistration() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put(METADATA, fabricMetadata("[ ]", "[ ]"));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("registers no mixin configs"));
}
@Test
public void rejectsBukkitSupertypeOutsidePlatformPackage() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/platform/generation/BukkitChunkGenerator.class",
classExtending("art/arcane/iris/platform/generation/BukkitChunkGenerator",
"org/bukkit/generator/ChunkGenerator"));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("org.bukkit supertype"));
}
@Test
public void rejectsBukkitInterfaceOutsidePlatformPackage() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/Listener.class",
classImplementing("art/arcane/iris/Listener", "org/bukkit/event/Listener"));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES));
assertTrue(failure.getMessage().contains("org.bukkit supertype"));
}
@Test
public void acceptsBukkitSupertypeInsidePlatformPackage() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/platform/bukkit/BukkitWorld.class",
classImplementing("art/arcane/iris/platform/bukkit/BukkitWorld", "org/bukkit/event/Listener"));
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES);
}
@Test
public void acceptsBukkitSupertypeDeclaredInThePurityAllowlist() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/platform/generation/BukkitChunkGenerator.class",
classExtending("art/arcane/iris/platform/generation/BukkitChunkGenerator",
"org/bukkit/generator/ChunkGenerator"));
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES,
Set.of("art/arcane/iris/platform/generation/BukkitChunkGenerator"));
}
@Test
public void acceptsBukkitSupertypeOnNestedClassOfAllowlistedSource() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/world/entity/IrisEntity$BukkitOps$1.class",
classImplementing("art/arcane/iris/world/entity/IrisEntity$BukkitOps$1",
"org/bukkit/event/Listener"));
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES,
Set.of("art/arcane/iris/world/entity/IrisEntity"));
}
@Test
public void acceptsBukkitSupertypeFromSharedLibrary() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/volmlib/util/bukkit/Events.class",
classImplementing("art/arcane/volmlib/util/bukkit/Events", "org/bukkit/event/Listener"));
File artifact = createArtifact(entries);
ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES);
}
@Test
public void rejectsBukkitSupertypeMissingFromThePurityAllowlist() throws Exception {
Map<String, byte[]> entries = validEntries();
entries.put("art/arcane/iris/integration/data/OraxenDataProvider.class",
classImplementing("art/arcane/iris/integration/data/OraxenDataProvider",
"org/bukkit/event/Listener"));
File artifact = createArtifact(entries);
GradleException failure = assertThrows(GradleException.class,
() -> ModdedArtifactVerifier.verify(artifact, REQUIRED_ENTRIES,
Set.of("art/arcane/iris/integration/data/ItemAdderDataProvider")));
assertTrue(failure.getMessage().contains("core/purity-allowlist.txt"));
}
private Map<String, byte[]> validEntries() {
Map<String, byte[]> entries = new LinkedHashMap<>();
entries.put(METADATA, fabricMetadata("[ ]", "[ \"" + MIXIN_CONFIG + "\" ]"));
entries.put(MIXIN_CONFIG, mixinConfig("JAVA_21"));
entries.put(CODEC_CLASS, INTERNAL_CODEC);
entries.put(HARDWARE_CLASS, "oshi/SystemInfo".getBytes(StandardCharsets.ISO_8859_1));
return entries;
}
private byte[] fabricMetadata(String jarsArray, String mixinsArray) {
return ("{\n"
+ " \"schemaVersion\": 1,\n"
+ " \"id\": \"irisworldgen\",\n"
+ " \"mixins\": " + mixinsArray + ",\n"
+ " \"jars\": " + jarsArray + "\n"
+ "}\n").getBytes(StandardCharsets.UTF_8);
}
private byte[] mixinConfig(String compatibilityLevel) {
return ("{\n"
+ " \"required\": true,\n"
+ " \"minVersion\": \"0.8\",\n"
+ " \"package\": \"art.arcane.iris.modded.mixin\",\n"
+ " \"compatibilityLevel\": \"" + compatibilityLevel + "\"\n"
+ "}\n").getBytes(StandardCharsets.UTF_8);
}
private Manifest manifestWithMixinConfigs(String configs) {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().putValue("MixinConfigs", configs);
return manifest;
}
private byte[] classExtending(String internalName, String superName) {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, superName, null);
writer.visitEnd();
return writer.toByteArray();
}
private byte[] classImplementing(String internalName, String interfaceName) {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object",
new String[]{interfaceName});
writer.visitEnd();
return writer.toByteArray();
}
private File createArtifact(Map<String, byte[]> entries) throws Exception {
return createArtifact(entries, null);
}
private File createArtifact(Map<String, byte[]> entries, Manifest manifest) throws Exception {
File artifact = temporaryFolder.newFile("artifact-" + System.nanoTime() + ".jar");
try (FileOutputStream file = new FileOutputStream(artifact);
JarOutputStream output = manifest == null
? new JarOutputStream(file)
: new JarOutputStream(file, manifest)) {
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
output.putNextEntry(new JarEntry(entry.getKey()));
output.write(entry.getValue());
output.closeEntry();
}
}
return artifact;
}
private byte[] createNestedArtifact(Map<String, byte[]> entries) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JarOutputStream output = new JarOutputStream(bytes)) {
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
output.putNextEntry(new JarEntry(entry.getKey()));
output.write(entry.getValue());
output.closeEntry();
}
}
return bytes.toByteArray();
}
}