diff --git a/archive/src/main/kotlin/org/openrs2/archive/client/ClientImporter.kt b/archive/src/main/kotlin/org/openrs2/archive/client/ClientImporter.kt index d1236c6c..a26b08ab 100644 --- a/archive/src/main/kotlin/org/openrs2/archive/client/ClientImporter.kt +++ b/archive/src/main/kotlin/org/openrs2/archive/client/ClientImporter.kt @@ -18,7 +18,9 @@ import jakarta.inject.Singleton import net.fornwall.jelf.ElfFile import net.fornwall.jelf.ElfSymbol import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.JumpInsnNode import org.objectweb.asm.tree.LdcInsnNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.TypeInsnNode @@ -26,6 +28,7 @@ import org.openrs2.archive.cache.CacheExporter import org.openrs2.archive.cache.CacheImporter import org.openrs2.asm.InsnMatcher import org.openrs2.asm.classpath.Library +import org.openrs2.asm.getArgumentExpressions import org.openrs2.asm.hasCode import org.openrs2.asm.intConstant import org.openrs2.asm.io.CabLibraryReader @@ -33,6 +36,7 @@ import org.openrs2.asm.io.JarLibraryReader import org.openrs2.asm.io.LibraryReader import org.openrs2.asm.io.Pack200LibraryReader import org.openrs2.asm.io.PackClassLibraryReader +import org.openrs2.asm.nextReal import org.openrs2.buffer.use import org.openrs2.compress.gzip.Gzip import org.openrs2.db.Database @@ -780,10 +784,157 @@ public class ClientImporter @Inject constructor( ) } - // TODO(gpe): new engine support + val loader = library["loader"] + if (loader != null) { + val links = mutableListOf() + val paths = mutableSetOf() + + for (method in loader.methods) { + if (method.name != "run" || method.desc != "()V") { + continue + } + + for (insn in method.instructions) { + if (insn !is MethodInsnNode || insn.owner != loader.name || !insn.desc.endsWith(")[B")) { + continue + } + + // TODO(gpe): extract file size too (tricky due to dummy arguments) + + val exprs = getArgumentExpressions(insn) ?: continue + for (expr in exprs) { + val single = expr.singleOrNull() ?: continue + if (single !is LdcInsnNode) { + continue + } + + val cst = single.cst + if (cst is String && FILE_NAME_REGEX.matches(cst)) { + paths += cst + } + } + } + } + + val hashes = mutableMapOf() + + for (method in loader.methods) { + for (match in SHA1_CMP_MATCHER.match(method)) { + val sha1 = ByteArray(SHA1_BYTES) + var i = 0 + + while (i < match.size) { + var n = match[i++].intConstant + if (n != null) { + i++ // ALOAD + } + + val index = match[i++].intConstant!! + i++ // BALOAD + + var xor = false + if (i + 1 < match.size && match[i + 1].opcode == Opcodes.IXOR) { + i += 2 // ICONST_M1, IXOR + xor = true + } + + if (match[i].opcode == Opcodes.IFNE) { + n = 0 + i++ + } else { + if (n == null) { + n = match[i++].intConstant!! + } + + i++ // ICMP_IFNE + } + + if (xor) { + n = n.inv() + } + + sha1[index] = n.toByte() + } + + hashes[match[0]] = sha1 + } + } + + for (method in loader.methods) { + for (match in PATH_CMP_MATCHER.match(method)) { + val first = match[0] + val ldc = if (first is LdcInsnNode) { + first + } else { + match[1] as LdcInsnNode + } + + val path = ldc.cst + if (path !is String) { + continue + } + + val acmp = match[2] as JumpInsnNode + val target = if (acmp.opcode == Opcodes.IF_ACMPNE) { + acmp.nextReal + } else { + acmp.label.nextReal + } + + val hash = hashes.remove(target) ?: continue + if (!paths.remove(path)) { + continue + } + + links += parseLink(path, hash) + } + } + + if (paths.size != hashes.size || paths.size > 1) { + throw IllegalArgumentException() + } else if (paths.size == 1) { + links += parseLink(paths.single(), hashes.values.single()) + } + + return links + } + + // TODO(gpe) return emptyList() } + private fun parseLink(path: String, sha1: ByteArray): ArtifactLink { + val m = FILE_NAME_REGEX.matchEntire(path) ?: throw IllegalArgumentException() + val (name, crc1, ext, crc2) = m.destructured + + val type = when (name) { + // TODO(gpe): funorb loaders + "runescape", "client" -> ArtifactType.CLIENT + "unpackclass" -> ArtifactType.UNPACKCLASS + else -> throw IllegalArgumentException() + } + + val format = when (ext) { + "pack200" -> ArtifactFormat.PACK200 + "js5" -> ArtifactFormat.PACKCLASS + "jar", "pack" -> ArtifactFormat.JAR + else -> throw IllegalArgumentException() + } + + val crc = crc1.toIntOrNull() ?: crc2.toIntOrNull() ?: throw IllegalArgumentException() + + return ArtifactLink( + type, + format, + OperatingSystem.INDEPENDENT, + Architecture.INDEPENDENT, + Jvm.INDEPENDENT, + crc, + sha1, + null + ) + } + private fun ByteBuf.hasPrefix(bytes: ByteArray): Boolean { Unpooled.wrappedBuffer(bytes).use { prefix -> val len = prefix.readableBytes() @@ -830,5 +981,10 @@ public class ClientImporter @Inject constructor( private const val SHA1_BYTES = 20 private val SHA1_MATCHER = InsnMatcher.compile("BIPUSH NEWARRAY (DUP (ICONST | BIPUSH) (ICONST | BIPUSH | SIPUSH) IASTORE)+") + + private val FILE_NAME_REGEX = Regex("([a-z]+)(?:_(-?[0-9]+))?[.]([a-z0-9]+)(?:\\?crc=(-?[0-9]+))?") + private val SHA1_CMP_MATCHER = + InsnMatcher.compile("((ICONST | BIPUSH)? ALOAD (ICONST | BIPUSH) BALOAD (ICONST IXOR)? (ICONST | BIPUSH)? (IF_ICMPEQ | IF_ICMPNE | IFEQ | IFNE))+") + private val PATH_CMP_MATCHER = InsnMatcher.compile("(LDC ALOAD | ALOAD LDC) (IF_ACMPEQ | IF_ACMPNE)") } }