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); + } +}