diff --git a/asm/pom.xml b/asm/pom.xml
index ff25349d..d019a3ab 100644
--- a/asm/pom.xml
+++ b/asm/pom.xml
@@ -12,4 +12,24 @@
jar
OpenRS2 ASM Utilities
+
+
+
+ dev.openrs2
+ openrs2-util
+ ${project.version}
+
+
+ org.ow2.asm
+ asm
+
+
+ org.ow2.asm
+ asm-tree
+
+
+ org.ow2.asm
+ asm-util
+
+
diff --git a/asm/src/main/java/dev/openrs2/asm/Library.java b/asm/src/main/java/dev/openrs2/asm/Library.java
new file mode 100644
index 00000000..05d8980f
--- /dev/null
+++ b/asm/src/main/java/dev/openrs2/asm/Library.java
@@ -0,0 +1,98 @@
+package dev.openrs2.asm;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.SequenceInputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Pack200;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import dev.openrs2.util.io.DeterministicJarOutputStream;
+import dev.openrs2.util.io.SkipOutputStream;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.util.CheckClassAdapter;
+
+public final class Library {
+ private static final String CLASS_SUFFIX = ".class";
+ private static final String TEMP_PREFIX = "tmp";
+ private static final String JAR_SUFFIX = ".jar";
+ private static final byte[] GZIP_HEADER = { 0x1f, (byte) 0x8b };
+
+ public static Library readJar(Path path) throws IOException {
+ var library = new Library();
+
+ try (var in = new JarInputStream(Files.newInputStream(path))) {
+ JarEntry entry;
+ while ((entry = in.getNextJarEntry()) != null) {
+ if (!entry.getName().endsWith(CLASS_SUFFIX)) {
+ continue;
+ }
+
+ var clazz = new ClassNode();
+ var reader = new ClassReader(in);
+ reader.accept(clazz, ClassReader.SKIP_DEBUG);
+
+ library.classes.put(clazz.name, clazz);
+ }
+ }
+
+ return library;
+ }
+
+ public static Library readPack(Path path) throws IOException {
+ var temp = Files.createTempFile(TEMP_PREFIX, JAR_SUFFIX);
+ try {
+ try (var header = new ByteArrayInputStream(GZIP_HEADER);
+ var data = Files.newInputStream(path);
+ var in = new GZIPInputStream(new SequenceInputStream(header, data));
+ var out = new JarOutputStream(Files.newOutputStream(temp))) {
+ Pack200.newUnpacker().unpack(in, out);
+ return readJar(temp);
+ }
+ } finally {
+ Files.deleteIfExists(temp);
+ }
+ }
+
+ private final Map classes = new TreeMap<>();
+
+ private Library() {
+ /* empty */
+ }
+
+ public void writeJar(Path path) throws IOException {
+ try (var out = new DeterministicJarOutputStream(Files.newOutputStream(path))) {
+ for (var entry : classes.entrySet()) {
+ var clazz = entry.getValue();
+ var writer = new ClassWriter(0);
+ clazz.accept(new CheckClassAdapter(writer, true));
+
+ out.putNextEntry(new JarEntry(clazz.name + CLASS_SUFFIX));
+ out.write(writer.toByteArray());
+ }
+ }
+ }
+
+ public void writePack(Path path) throws IOException {
+ var temp = Files.createTempFile(TEMP_PREFIX, JAR_SUFFIX);
+ try {
+ writeJar(temp);
+
+ try (var in = new JarInputStream(Files.newInputStream(temp));
+ var out = new GZIPOutputStream(new SkipOutputStream(Files.newOutputStream(path), 2))) {
+ Pack200.newPacker().pack(in, out);
+ }
+ } finally {
+ Files.deleteIfExists(temp);
+ }
+ }
+}
diff --git a/util/src/main/java/dev/openrs2/util/io/DeterministicJarOutputStream.java b/util/src/main/java/dev/openrs2/util/io/DeterministicJarOutputStream.java
new file mode 100644
index 00000000..118bb28b
--- /dev/null
+++ b/util/src/main/java/dev/openrs2/util/io/DeterministicJarOutputStream.java
@@ -0,0 +1,28 @@
+package dev.openrs2.util.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.attribute.FileTime;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+
+public final class DeterministicJarOutputStream extends JarOutputStream {
+ private static final FileTime UNIX_EPOCH = FileTime.fromMillis(0);
+
+ public DeterministicJarOutputStream(OutputStream out) throws IOException {
+ super(out);
+ }
+
+ public DeterministicJarOutputStream(OutputStream out, Manifest man) throws IOException {
+ super(out, man);
+ }
+
+ @Override
+ public void putNextEntry(ZipEntry ze) throws IOException {
+ ze.setCreationTime(UNIX_EPOCH);
+ ze.setLastAccessTime(UNIX_EPOCH);
+ ze.setLastModifiedTime(UNIX_EPOCH);
+ super.putNextEntry(ze);
+ }
+}
diff --git a/util/src/main/java/dev/openrs2/util/io/SkipOutputStream.java b/util/src/main/java/dev/openrs2/util/io/SkipOutputStream.java
new file mode 100644
index 00000000..3ac68f4c
--- /dev/null
+++ b/util/src/main/java/dev/openrs2/util/io/SkipOutputStream.java
@@ -0,0 +1,37 @@
+package dev.openrs2.util.io;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public final class SkipOutputStream extends FilterOutputStream {
+ private long skipBytes;
+
+ public SkipOutputStream(OutputStream out, long skipBytes) {
+ super(out);
+ this.skipBytes = skipBytes;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ if (skipBytes == 0) {
+ super.write(b);
+ } else {
+ skipBytes--;
+ }
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (len >= skipBytes) {
+ off += skipBytes;
+ len -= skipBytes;
+ skipBytes = 0;
+ } else {
+ skipBytes -= len;
+ return;
+ }
+
+ super.write(b, off, len);
+ }
+}