diff --git a/asm/pom.xml b/asm/pom.xml index d019a3ab..91d95e20 100644 --- a/asm/pom.xml +++ b/asm/pom.xml @@ -23,6 +23,10 @@ org.ow2.asm asm + + org.ow2.asm + asm-commons + org.ow2.asm asm-tree diff --git a/asm/src/main/java/dev/openrs2/asm/Library.java b/asm/src/main/java/dev/openrs2/asm/Library.java index 497da164..8a670ebb 100644 --- a/asm/src/main/java/dev/openrs2/asm/Library.java +++ b/asm/src/main/java/dev/openrs2/asm/Library.java @@ -18,7 +18,10 @@ 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.ClassVisitor; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.Remapper; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.util.CheckClassAdapter; import org.slf4j.Logger; @@ -107,26 +110,33 @@ public final class Library implements Iterable { return classes.values().iterator(); } - public void writeJar(Path path) throws IOException { + public void writeJar(Path path, Remapper remapper) throws IOException { logger.info("Writing jar {}", path); try (var out = new DeterministicJarOutputStream(Files.newOutputStream(path))) { for (var clazz : classes.values()) { + var name = clazz.name; var writer = new ClassWriter(0); - clazz.accept(new CheckClassAdapter(writer, true)); - out.putNextEntry(new JarEntry(clazz.name + CLASS_SUFFIX)); + ClassVisitor visitor = new CheckClassAdapter(writer, true); + if (remapper != null) { + visitor = new ClassRemapper(visitor, remapper); + name = remapper.map(name); + } + clazz.accept(visitor); + + out.putNextEntry(new JarEntry(name + CLASS_SUFFIX)); out.write(writer.toByteArray()); } } } - public void writePack(Path path) throws IOException { + public void writePack(Path path, Remapper remapper) throws IOException { logger.info("Writing pack {}", path); var temp = Files.createTempFile(TEMP_PREFIX, JAR_SUFFIX); try { - writeJar(temp); + writeJar(temp, remapper); try (var in = new JarInputStream(Files.newInputStream(temp)); var out = new GZIPOutputStream(new SkipOutputStream(Files.newOutputStream(path), 2))) { diff --git a/asm/src/main/java/dev/openrs2/asm/MemberDesc.java b/asm/src/main/java/dev/openrs2/asm/MemberDesc.java new file mode 100644 index 00000000..0ac941aa --- /dev/null +++ b/asm/src/main/java/dev/openrs2/asm/MemberDesc.java @@ -0,0 +1,43 @@ +package dev.openrs2.asm; + +import java.util.Objects; + +public final class MemberDesc { + private final String name, desc; + + public MemberDesc(String name, String desc) { + this.name = name; + this.desc = desc; + } + + public String getName() { + return name; + } + + public String getDesc() { + return desc; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MemberDesc fieldRef = (MemberDesc) o; + return name.equals(fieldRef.name) && + desc.equals(fieldRef.desc); + } + + @Override + public int hashCode() { + return Objects.hash(name, desc); + } + + @Override + public String toString() { + return String.format("%s %s", desc, name); + } +} diff --git a/asm/src/main/java/dev/openrs2/asm/MemberRef.java b/asm/src/main/java/dev/openrs2/asm/MemberRef.java index f4a7e549..3bff5559 100644 --- a/asm/src/main/java/dev/openrs2/asm/MemberRef.java +++ b/asm/src/main/java/dev/openrs2/asm/MemberRef.java @@ -5,6 +5,10 @@ import java.util.Objects; public final class MemberRef { private final String owner, name, desc; + public MemberRef(String owner, MemberDesc desc) { + this(owner, desc.getName(), desc.getDesc()); + } + public MemberRef(String owner, String name, String desc) { this.owner = owner; this.name = name; diff --git a/deob/src/main/java/dev/openrs2/deob/ClassNamePrefixer.java b/deob/src/main/java/dev/openrs2/deob/ClassNamePrefixer.java new file mode 100644 index 00000000..f1d424a9 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/ClassNamePrefixer.java @@ -0,0 +1,36 @@ +package dev.openrs2.deob; + +import java.util.HashMap; + +import dev.openrs2.asm.Library; +import dev.openrs2.deob.path.TypedRemapper; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.SimpleRemapper; +import org.objectweb.asm.tree.ClassNode; + +public final class ClassNamePrefixer { + public static void addPrefix(Library library, String prefix) { + var mapping = new HashMap(); + for (var clazz : library) { + if (TypedRemapper.EXCLUDED_CLASSES.contains(clazz.name)) { + mapping.put(clazz.name, clazz.name); + } else { + mapping.put(clazz.name, prefix + clazz.name); + } + } + var remapper = new SimpleRemapper(mapping); + + for (var name : mapping.keySet()) { + var in = library.remove(name); + + var out = new ClassNode(); + in.accept(new ClassRemapper(out, remapper)); + + library.add(out); + } + } + + private ClassNamePrefixer() { + /* empty */ + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java index 6ed671c3..4606bb04 100644 --- a/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java +++ b/deob/src/main/java/dev/openrs2/deob/Deobfuscator.java @@ -1,6 +1,9 @@ package dev.openrs2.deob; import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; @@ -8,6 +11,9 @@ import java.util.Map; import dev.openrs2.asm.Library; import dev.openrs2.asm.Transformer; +import dev.openrs2.deob.path.ClassPath; +import dev.openrs2.deob.path.TypedRemapper; +import dev.openrs2.deob.transform.ClassForNameTransformer; import dev.openrs2.deob.transform.OpaquePredicateTransformer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,32 +26,53 @@ public final class Deobfuscator { ); public static void main(String[] args) throws IOException { - var deobfuscator = new Deobfuscator(Paths.get("nonfree/code")); + var deobfuscator = new Deobfuscator(Paths.get("nonfree/code"), Paths.get("nonfree/code/deob")); deobfuscator.run(); } - private final Path input; + private final Path input, output; - public Deobfuscator(Path input) { + public Deobfuscator(Path input, Path output) { this.input = input; + this.output = output; } public void run() throws IOException { + /* read input jars/packs */ + logger.info("Reading input jars"); var unpacker = Library.readJar(input.resolve("game_unpacker.dat")); + var glUnpacker = new Library(unpacker); var loader = Library.readJar(input.resolve("loader.jar")); var glLoader = Library.readJar(input.resolve("loader_gl.jar")); var client = Library.readJar(input.resolve("runescape.jar")); var glClient = Library.readPack(input.resolve("runescape_gl.pack200")); + var unsignedClient = new Library(client); - var libraries = Map.of( + /* read dependencies */ + var runtime = ClassLoader.getPlatformClassLoader(); + var jogl = new URLClassLoader(new URL[] { + input.resolve("jogl.jar").toUri().toURL() + }, runtime); + + /* overwrite client's classes with signed classes from the loader */ + logger.info("Moving signed classes from loader to runescape"); + var signedClasses = SignedClassSet.create(loader, client); + + logger.info("Moving signed classes from loader_gl to runescape_gl"); + var glSignedClasses = SignedClassSet.create(glLoader, glClient); + + /* deobfuscate */ + var allLibraries = Map.of( "unpacker", unpacker, + "unpacker_gl", glUnpacker, "loader", loader, "loader_gl", glLoader, "runescape", client, - "runescape_gl", glClient + "runescape_gl", glClient, + "runescape_unsigned", unsignedClient ); - for (var entry : libraries.entrySet()) { + for (var entry : allLibraries.entrySet()) { logger.info("Transforming library {}", entry.getKey()); for (var transformer : TRANSFORMERS) { @@ -53,5 +80,74 @@ public final class Deobfuscator { transformer.transform(entry.getValue()); } } + + /* move unpack class out of the loader (so the unpacker and loader can both depend on it) */ + logger.info("Moving unpack from loader to unpack"); + var unpack = new Library(); + unpack.add(loader.remove("unpack")); + + logger.info("Moving unpack from loader_gl to unpack_gl"); + var glUnpack = new Library(); + glUnpack.add(glLoader.remove("unpack")); + + /* move signed classes out of the client (so the client and loader can both depend on them) */ + logger.info("Moving signed classes from runescape to signlink"); + var signLink = new Library(); + signedClasses.move(client, signLink); + + logger.info("Moving signed classes from runescape_gl to signlink_gl"); + var glSignLink = new Library(); + glSignedClasses.move(glClient, glSignLink); + + /* prefix remaining loader/unpacker classes (to avoid conflicts when we rename in the same classpath as the client) */ + logger.info("Prefixing loader and unpacker class names"); + ClassNamePrefixer.addPrefix(loader, "loader_"); + ClassNamePrefixer.addPrefix(glLoader, "loader_"); + ClassNamePrefixer.addPrefix(unpacker, "unpacker_"); + ClassNamePrefixer.addPrefix(glUnpacker, "unpacker_"); + + /* remap all class, method and field names */ + logger.info("Creating remappers"); + var libraries = new Library[] { client, loader, signLink, unpack, unpacker }; + var remapper = TypedRemapper.create(new ClassPath(runtime, libraries)); + + var glLibraries = new Library[] { glClient, glLoader, glSignLink, glUnpack, glUnpacker }; + var glRemapper = TypedRemapper.create(new ClassPath(jogl, glLibraries)); + + var unsignedRemapper = TypedRemapper.create(new ClassPath(runtime, unsignedClient)); + + /* transform Class.forName() calls */ + logger.info("Transforming Class.forName() calls"); + var transformer = new ClassForNameTransformer(remapper); + for (var library : libraries) { + transformer.transform(library); + } + + var glTransformer = new ClassForNameTransformer(glRemapper); + for (var library : glLibraries) { + glTransformer.transform(library); + } + + var unsignedTransformer = new ClassForNameTransformer(unsignedRemapper); + unsignedTransformer.transform(unsignedClient); + + /* write output jars */ + logger.info("Writing output jars"); + + Files.createDirectories(output); + + client.writeJar(output.resolve("runescape.jar"), remapper); + loader.writeJar(output.resolve("loader.jar"), remapper); + signLink.writeJar(output.resolve("signlink.jar"), remapper); + unpack.writeJar(output.resolve("unpack.jar"), remapper); + unpacker.writeJar(output.resolve("unpacker.jar"), remapper); + + glClient.writeJar(output.resolve("runescape_gl.jar"), glRemapper); + glLoader.writeJar(output.resolve("loader_gl.jar"), glRemapper); + glSignLink.writeJar(output.resolve("signlink_gl.jar"), glRemapper); + glUnpack.writeJar(output.resolve("unpack_gl.jar"), glRemapper); + glUnpacker.writeJar(output.resolve("unpacker_gl.jar"), glRemapper); + + unsignedClient.writeJar(output.resolve("runescape_unsigned.jar"), unsignedRemapper); } } diff --git a/deob/src/main/java/dev/openrs2/deob/SignedClassSet.java b/deob/src/main/java/dev/openrs2/deob/SignedClassSet.java new file mode 100644 index 00000000..0e2ef635 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/SignedClassSet.java @@ -0,0 +1,114 @@ +package dev.openrs2.deob; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.Sets; +import dev.openrs2.asm.InsnMatcher; +import dev.openrs2.asm.Library; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.SimpleRemapper; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class SignedClassSet { + private static final Logger logger = LoggerFactory.getLogger(SignedClassSet.class); + + private static final InsnMatcher LOAD_SIGNED_CLASS_MATCHER = InsnMatcher.compile("LDC INVOKESTATIC ASTORE ALOAD GETFIELD ALOAD INVOKEVIRTUAL ALOAD INVOKEVIRTUAL POP"); + + public static SignedClassSet create(Library loader, Library client) { + /* find signed classes */ + var signedClasses = findSignedClasses(loader); + logger.info("Identified signed classes {}", signedClasses); + + var dependencies = findDependencies(loader, signedClasses); + logger.info("Identified signed class dependencies {}", dependencies); + + /* rename dependencies of signed classes so they don't clash with client classes */ + var mapping = new HashMap(); + for (var dependency : dependencies) { + mapping.put(dependency, "loader_" + dependency); + } + var remapper = new SimpleRemapper(mapping); + + /* move signed classes to the client */ + var remappedSignedClasses = new HashSet(); + for (var name : Sets.union(signedClasses, dependencies)) { + var in = loader.remove(name); + + var out = new ClassNode(); + in.accept(new ClassRemapper(out, remapper)); + + remappedSignedClasses.add(out.name); + client.add(out); + } + return new SignedClassSet(remappedSignedClasses); + } + + private static Set findSignedClasses(Library loader) { + var clazz = loader.get("loader"); + if (clazz == null) { + throw new IllegalArgumentException("Failed to find loader class"); + } + + for (var method : clazz.methods) { + if (method.name.equals("run") && method.desc.equals("()V")) { + return findSignedClasses(method); + } + } + + throw new IllegalArgumentException("Failed to find loader.run() method"); + } + + private static Set findSignedClasses(MethodNode method) { + var classes = new HashSet(); + + LOAD_SIGNED_CLASS_MATCHER.match(method).forEach(match -> { + var ldc = (LdcInsnNode) match.get(0); + if (ldc.cst instanceof String && !ldc.cst.equals("unpack")) { + classes.add((String) ldc.cst); + } + }); + + return classes; + } + + private static Set findDependencies(Library loader, Set signedClasses) { + var dependencies = new HashSet(); + + for (var signedClass : signedClasses) { + var clazz = loader.get(signedClass); + + for (var field : clazz.fields) { + var type = Type.getType(field.desc); + if (type.getSort() != Type.OBJECT) { + continue; + } + + var name = type.getClassName(); + if (loader.contains(name) && !signedClasses.contains(name)) { + dependencies.add(name); + } + } + } + + return dependencies; + } + + private final Set signedClasses; + + private SignedClassSet(Set signedClasses) { + this.signedClasses = signedClasses; + } + + public void move(Library client, Library signLink) { + for (var name : signedClasses) { + signLink.add(client.remove(name)); + } + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/path/AsmClassMetadata.java b/deob/src/main/java/dev/openrs2/deob/path/AsmClassMetadata.java new file mode 100644 index 00000000..f0dbd77e --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/path/AsmClassMetadata.java @@ -0,0 +1,72 @@ +package dev.openrs2.deob.path; + +import java.util.List; +import java.util.stream.Collectors; + +import dev.openrs2.asm.MemberDesc; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; + +public final class AsmClassMetadata extends ClassMetadata { + private final ClassPath classPath; + private final ClassNode clazz; + + public AsmClassMetadata(ClassPath classPath, ClassNode clazz) { + this.classPath = classPath; + this.clazz = clazz; + } + + @Override + public String getName() { + return clazz.name; + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public boolean isInterface() { + return (clazz.access & Opcodes.ACC_INTERFACE) != 0; + } + + @Override + public ClassMetadata getSuperClass() { + if (clazz.superName != null) { + return classPath.get(clazz.superName); + } + return null; + } + + @Override + public List getSuperInterfaces() { + return clazz.interfaces.stream() + .map(classPath::get) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public List getFields() { + return clazz.fields.stream() + .map(f -> new MemberDesc(f.name, f.desc)) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public List getMethods() { + return clazz.methods.stream() + .map(m -> new MemberDesc(m.name, m.desc)) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public boolean isNative(MemberDesc method) { + for (var m : clazz.methods) { + if (m.name.equals(method.getName()) && m.desc.equals(method.getDesc())) { + return (m.access & Opcodes.ACC_NATIVE) != 0; + } + } + return false; + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/path/ClassMetadata.java b/deob/src/main/java/dev/openrs2/deob/path/ClassMetadata.java new file mode 100644 index 00000000..9117160d --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/path/ClassMetadata.java @@ -0,0 +1,34 @@ +package dev.openrs2.deob.path; + +import java.util.List; +import java.util.Objects; + +import dev.openrs2.asm.MemberDesc; + +public abstract class ClassMetadata { + public abstract String getName(); + public abstract boolean isMutable(); + public abstract boolean isInterface(); + public abstract ClassMetadata getSuperClass(); + public abstract List getSuperInterfaces(); + public abstract List getFields(); + public abstract List getMethods(); + public abstract boolean isNative(MemberDesc method); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + var that = (ClassMetadata) o; + return getName().equals(that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getName()); + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/path/ClassPath.java b/deob/src/main/java/dev/openrs2/deob/path/ClassPath.java new file mode 100644 index 00000000..98927593 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/path/ClassPath.java @@ -0,0 +1,61 @@ +package dev.openrs2.deob.path; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import dev.openrs2.asm.Library; + +public final class ClassPath { + private final ClassLoader dependencyLoader; + private final List libraries; + private final Map cache = new HashMap<>(); + + public ClassPath(ClassLoader dependencyLoader, Library... libraries) { + this.dependencyLoader = dependencyLoader; + this.libraries = List.of(libraries); + } + + public List getLibraryClasses() { + var classes = new ArrayList(); + + for (var library : libraries) { + for (var clazz : library) { + classes.add(get(clazz.name)); + } + } + + return Collections.unmodifiableList(classes); + } + + public ClassMetadata get(String name) { + var metadata = cache.get(name); + if (metadata != null) { + return metadata; + } + + for (var library : libraries) { + var clazz = library.get(name); + if (clazz != null) { + metadata = new AsmClassMetadata(this, clazz); + cache.put(name, metadata); + return metadata; + } + } + + var reflectionName = name.replace('/', '.'); + + Class clazz; + try { + clazz = dependencyLoader.loadClass(reflectionName); + } catch (ClassNotFoundException ex) { + throw new IllegalArgumentException("Unknown class " + name); + } + + metadata = new ReflectionClassMetadata(this, clazz); + cache.put(name, metadata); + return metadata; + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/path/ReflectionClassMetadata.java b/deob/src/main/java/dev/openrs2/deob/path/ReflectionClassMetadata.java new file mode 100644 index 00000000..c6a9d6aa --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/path/ReflectionClassMetadata.java @@ -0,0 +1,74 @@ +package dev.openrs2.deob.path; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import dev.openrs2.asm.MemberDesc; +import org.objectweb.asm.Type; + +public final class ReflectionClassMetadata extends ClassMetadata { + private final ClassPath classPath; + private final Class clazz; + + public ReflectionClassMetadata(ClassPath classPath, Class clazz) { + this.classPath = classPath; + this.clazz = clazz; + } + + @Override + public String getName() { + return clazz.getName().replace('.', '/'); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public boolean isInterface() { + return clazz.isInterface(); + } + + @Override + public ClassMetadata getSuperClass() { + var superClass = clazz.getSuperclass(); + if (superClass != null) { + return classPath.get(superClass.getName().replace('.', '/')); + } + return null; + } + + @Override + public List getSuperInterfaces() { + return Arrays.stream(clazz.getInterfaces()) + .map(i -> classPath.get(i.getName().replace('.', '/'))) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public List getFields() { + return Arrays.stream(clazz.getDeclaredFields()) + .map(f -> new MemberDesc(f.getName(), Type.getDescriptor(f.getType()))) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public List getMethods() { + return Arrays.stream(clazz.getDeclaredMethods()) + .map(m -> new MemberDesc(m.getName(), Type.getMethodDescriptor(m))) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public boolean isNative(MemberDesc method) { + for (var m : clazz.getDeclaredMethods()) { + if (m.getName().equals(method.getName()) && Type.getMethodDescriptor(m).equals(method.getDesc())) { + return Modifier.isNative(m.getModifiers()); + } + } + return false; + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/path/TypedRemapper.java b/deob/src/main/java/dev/openrs2/deob/path/TypedRemapper.java new file mode 100644 index 00000000..0181ac48 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/path/TypedRemapper.java @@ -0,0 +1,320 @@ +package dev.openrs2.deob.path; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.base.Strings; +import dev.openrs2.asm.MemberDesc; +import dev.openrs2.asm.MemberRef; +import dev.openrs2.util.StringUtils; +import dev.openrs2.util.collect.DisjointSet; +import dev.openrs2.util.collect.ForestDisjointSet; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.Remapper; + +public final class TypedRemapper extends Remapper { + public static final Set EXCLUDED_CLASSES = Set.of( + "client", + "jagex3/jagmisc/jagmisc", + "loader", + "unpack", + "unpackclass" + ); + public static final Set EXCLUDED_METHODS = Set.of( + "", + "", + "main", + "providesignlink", + "quit" + ); + public static final Set EXCLUDED_FIELDS = Set.of( + "cache" + ); + + public static TypedRemapper create(ClassPath classPath) { + var libraryClasses = classPath.getLibraryClasses(); + + var inheritedFieldSets = createInheritedFieldSets(libraryClasses); + var inheritedMethodSets = createInheritedMethodSets(libraryClasses); + + var classes = createClassMapping(libraryClasses); + var fields = createFieldMapping(classPath, inheritedFieldSets, classes); + var methods = createMethodMapping(classPath, inheritedMethodSets); + + return new TypedRemapper(classes, fields, methods); + } + + private static DisjointSet createInheritedFieldSets(List classes) { + var disjointSet = new ForestDisjointSet(); + var ancestorCache = new HashMap>(); + + for (var clazz : classes) { + populateInheritedFieldSets(ancestorCache, disjointSet, clazz); + } + + return disjointSet; + } + + private static Set populateInheritedFieldSets(Map> ancestorCache, DisjointSet disjointSet, ClassMetadata clazz) { + var ancestors = ancestorCache.get(clazz); + if (ancestors != null) { + return ancestors; + } + ancestors = new HashSet<>(); + + var superClass = clazz.getSuperClass(); + if (superClass != null) { + var fields = populateInheritedFieldSets(ancestorCache, disjointSet, superClass); + + for (var field : fields) { + var partition1 = disjointSet.add(new MemberRef(clazz.getName(), field)); + var partition2 = disjointSet.add(new MemberRef(superClass.getName(), field)); + disjointSet.union(partition1, partition2); + } + + ancestors.addAll(fields); + } + + for (var superInterface : clazz.getSuperInterfaces()) { + var fields = populateInheritedFieldSets(ancestorCache, disjointSet, superInterface); + + for (var field : fields) { + var partition1 = disjointSet.add(new MemberRef(clazz.getName(), field)); + var partition2 = disjointSet.add(new MemberRef(superInterface.getName(), field)); + disjointSet.union(partition1, partition2); + } + + ancestors.addAll(fields); + } + + for (var field : clazz.getFields()) { + if (EXCLUDED_FIELDS.contains(field.getName())) { + continue; + } + + disjointSet.add(new MemberRef(clazz.getName(), field)); + ancestors.add(field); + } + + ancestors = Collections.unmodifiableSet(ancestors); + ancestorCache.put(clazz, ancestors); + return ancestors; + } + + private static DisjointSet createInheritedMethodSets(List classes) { + var disjointSet = new ForestDisjointSet(); + var ancestorCache = new HashMap>(); + + for (var clazz : classes) { + populateInheritedMethodSets(ancestorCache, disjointSet, clazz); + } + + return disjointSet; + } + + private static Set populateInheritedMethodSets(Map> ancestorCache, DisjointSet disjointSet, ClassMetadata clazz) { + var ancestors = ancestorCache.get(clazz); + if (ancestors != null) { + return ancestors; + } + ancestors = new HashSet<>(); + + var superClass = clazz.getSuperClass(); + if (superClass != null) { + var methods = populateInheritedMethodSets(ancestorCache, disjointSet, superClass); + + for (var method : methods) { + var partition1 = disjointSet.add(new MemberRef(clazz.getName(), method)); + var partition2 = disjointSet.add(new MemberRef(superClass.getName(), method)); + disjointSet.union(partition1, partition2); + } + + ancestors.addAll(methods); + } + + for (var superInterface : clazz.getSuperInterfaces()) { + var methods = populateInheritedMethodSets(ancestorCache, disjointSet, superInterface); + + for (var method : methods) { + var partition1 = disjointSet.add(new MemberRef(clazz.getName(), method)); + var partition2 = disjointSet.add(new MemberRef(superInterface.getName(), method)); + disjointSet.union(partition1, partition2); + } + + ancestors.addAll(methods); + } + + for (var method : clazz.getMethods()) { + if (EXCLUDED_METHODS.contains(method.getName())) { + continue; + } + + disjointSet.add(new MemberRef(clazz.getName(), method)); + ancestors.add(method); + } + + ancestors = Collections.unmodifiableSet(ancestors); + ancestorCache.put(clazz, ancestors); + return ancestors; + } + + private static String generateName(Map prefixes, String prefix) { + return prefix + prefixes.merge(prefix, 1, Integer::sum); + } + + private static Map createClassMapping(List classes) { + var mapping = new HashMap(); + var prefixes = new HashMap(); + + for (var clazz : classes) { + populateClassMapping(mapping, prefixes, clazz); + } + + return mapping; + } + + private static String populateClassMapping(Map mapping, Map prefixes, ClassMetadata clazz) { + var name = clazz.getName(); + if (mapping.containsKey(name) || EXCLUDED_CLASSES.contains(name) || !clazz.isMutable()) { + return mapping.getOrDefault(name, name); + } + + var mappedName = name.substring(0, name.lastIndexOf('/') + 1); + + var superClass = clazz.getSuperClass(); + if (superClass != null && !superClass.getName().equals("java/lang/Object")) { + var superName = populateClassMapping(mapping, prefixes, superClass); + superName = superName.substring(superName.lastIndexOf('/') + 1); + + mappedName += generateName(prefixes, superName + "_Sub"); + } else if (clazz.isInterface()) { + mappedName += generateName(prefixes, "Interface"); + } else { + mappedName += generateName(prefixes, "Class"); + } + + mapping.put(name, mappedName); + return mappedName; + } + + private static Map createFieldMapping(ClassPath classPath, DisjointSet disjointSet, Map classMapping) { + var mapping = new HashMap(); + var prefixes = new HashMap(); + + for (var partition : disjointSet) { + boolean skip = false; + + for (var field : partition) { + var clazz = classPath.get(field.getOwner()); + + if (!clazz.isMutable()) { + skip = true; + break; + } + } + + if (skip) { + continue; + } + + var prefix = ""; + + var type = Type.getType(partition.iterator().next().getDesc()); + if (type.getSort() == Type.ARRAY) { + prefix = Strings.repeat("Array", type.getDimensions()); + type = type.getElementType(); + } + + switch (type.getSort()) { + case Type.BOOLEAN: + case Type.BYTE: + case Type.CHAR: + case Type.SHORT: + case Type.INT: + case Type.LONG: + case Type.FLOAT: + case Type.DOUBLE: + prefix = type.getClassName() + prefix; + break; + case Type.OBJECT: + var className = classMapping.getOrDefault(type.getInternalName(), type.getInternalName()); + className = className.substring(className.lastIndexOf('/') + 1); + prefix = className + prefix; + break; + default: + throw new IllegalArgumentException("Unknown field type " + type); + } + + prefix = StringUtils.indefiniteArticle(prefix) + StringUtils.capitalize(prefix); + + var mappedName = generateName(prefixes, prefix); + for (var field : partition) { + mapping.put(field, mappedName); + } + } + + return mapping; + } + + private static Map createMethodMapping(ClassPath classPath, DisjointSet disjointSet) { + var mapping = new HashMap(); + var id = 0; + + for (var partition : disjointSet) { + boolean skip = false; + + for (var method : partition) { + var clazz = classPath.get(method.getOwner()); + + if (!clazz.isMutable()) { + skip = true; + break; + } + + if (clazz.isNative(new MemberDesc(method.getName(), method.getDesc()))) { + skip = true; + break; + } + } + + if (skip) { + continue; + } + + var mappedName = "method" + (++id); + for (var method : partition) { + mapping.put(method, mappedName); + } + } + + return mapping; + } + + private final Map classes; + private final Map fields, methods; + + private TypedRemapper(Map classes, Map fields, Map methods) { + this.classes = classes; + this.fields = fields; + this.methods = methods; + } + + @Override + public String map(String internalName) { + return classes.getOrDefault(internalName, internalName); + } + + @Override + public String mapFieldName(String owner, String name, String descriptor) { + return fields.getOrDefault(new MemberRef(owner, name, descriptor), name); + } + + @Override + public String mapMethodName(String owner, String name, String descriptor) { + return methods.getOrDefault(new MemberRef(owner, name, descriptor), name); + } +} diff --git a/deob/src/main/java/dev/openrs2/deob/transform/ClassForNameTransformer.java b/deob/src/main/java/dev/openrs2/deob/transform/ClassForNameTransformer.java new file mode 100644 index 00000000..b0ede053 --- /dev/null +++ b/deob/src/main/java/dev/openrs2/deob/transform/ClassForNameTransformer.java @@ -0,0 +1,42 @@ +package dev.openrs2.deob.transform; + +import java.util.List; + +import dev.openrs2.asm.InsnMatcher; +import dev.openrs2.asm.Transformer; +import org.objectweb.asm.commons.Remapper; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +public final class ClassForNameTransformer extends Transformer { + private static final InsnMatcher INVOKE_MATCHER = InsnMatcher.compile("LDC INVOKESTATIC"); + + private static boolean isClassForName(List match) { + var ldc = (LdcInsnNode) match.get(0); + if (!(ldc.cst instanceof String)) { + return false; + } + + var invokestatic = (MethodInsnNode) match.get(1); + return invokestatic.owner.equals("java/lang/Class") && + invokestatic.name.equals("forName") && + invokestatic.desc.equals("(Ljava/lang/String;)Ljava/lang/Class;"); + } + + private final Remapper remapper; + + public ClassForNameTransformer(Remapper remapper) { + this.remapper = remapper; + } + + @Override + public void transformMethod(ClassNode clazz, MethodNode method) { + INVOKE_MATCHER.match(method).filter(ClassForNameTransformer::isClassForName).forEach(match -> { + var ldc = (LdcInsnNode) match.get(0); + ldc.cst = remapper.map((String) ldc.cst); + }); + } +} diff --git a/util/src/main/java/dev/openrs2/util/StringUtils.java b/util/src/main/java/dev/openrs2/util/StringUtils.java new file mode 100644 index 00000000..a3913c1c --- /dev/null +++ b/util/src/main/java/dev/openrs2/util/StringUtils.java @@ -0,0 +1,29 @@ +package dev.openrs2.util; + +import com.google.common.base.Preconditions; + +public final class StringUtils { + public static String indefiniteArticle(String str) { + Preconditions.checkArgument(!str.isEmpty()); + + var first = Character.toLowerCase(str.charAt(0)); + if (first == 'a' || first == 'e' || first == 'i' || first == 'o' || first == 'u') { + return "an"; + } else { + return "a"; + } + } + + public static String capitalize(String str) { + if (str.isEmpty()) { + return str; + } + + var first = Character.toUpperCase(str.charAt(0)); + return first + str.substring(1); + } + + private StringUtils() { + /* empty */ + } +}