Refactor Patcher

This commit is contained in:
Lucaskyy 2022-03-19 18:35:41 +01:00
parent 733fb6a3b8
commit a9e7f19d51
No known key found for this signature in database
GPG Key ID: 1530BFF96D1EEB89
17 changed files with 192 additions and 159 deletions

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
</set>
</option>
</component>
</project>

View File

@ -16,7 +16,6 @@ dependencies {
implementation("org.ow2.asm:asm-util:9.2")
implementation("org.ow2.asm:asm-tree:9.2")
implementation("org.ow2.asm:asm-commons:9.2")
implementation("com.google.code.gson:gson:2.9.0")
testImplementation(kotlin("test"))
}

View File

@ -1,2 +1 @@
rootProject.name = "ReVanced Patcher"
rootProject.name = "revanced-patcher"

View File

@ -1,38 +0,0 @@
package net.revanced.patcher
import net.revanced.patcher.signature.model.Signature
import org.objectweb.asm.tree.InsnList
import org.objectweb.asm.tree.MethodNode
internal class MethodResolver(private val targetMethods: List<MethodNode>, private val signatures: List<Signature>) {
fun resolve(): MutableMap<String, MethodNode> {
val methods = mutableMapOf<String, MethodNode>()
for (signature in signatures) {
val method = targetMethods.firstOrNull { method ->
method.access == signature.accessors &&
signature.parameters.all { parameter ->
method.parameters.any { methodParameter ->
true //TODO check for parameter element type
}
} && method.instructions.scanFor(signature.opcodes)
} ?: continue
methods[signature.name] = method
}
return methods
}
}
//TODO: implement returning the index of the needle in the hay
private fun InsnList.scanFor(pattern: Array<Int>): Boolean {
for (i in 0 until this.size()) {
var occurrence = 0
while (i + occurrence < this.size()) {
if (this.get(i + occurrence).opcode != pattern.get(occurrence)) break
if (++occurrence >= pattern.size) return true
}
}
return false
}

View File

@ -2,54 +2,45 @@ package net.revanced.patcher
import net.revanced.patcher.cache.Cache
import net.revanced.patcher.patch.Patch
import net.revanced.patcher.signature.model.Signature
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
import java.io.File
import net.revanced.patcher.patch.PatchResult
import net.revanced.patcher.resolver.MethodResolver
import net.revanced.patcher.signature.Signature
import net.revanced.patcher.util.Jar2ASM
import java.io.InputStream
import java.util.jar.JarFile
class Patcher private constructor(
file: File,
signatures: List<Signature>
/**
* The patcher. (docs WIP)
*
* @param input the input stream to read from, must be a JAR file (for now)
*/
class Patcher (
input: InputStream,
signatures: Array<Signature>,
) {
val cache = Cache()
private val patches: MutableList<Patch> = mutableListOf()
init {
// collecting all methods here
val targetMethods: MutableList<MethodNode> = mutableListOf()
val jarFile = JarFile(file)
jarFile.stream().forEach { jarEntry ->
jarFile.getInputStream(jarEntry).use { jis ->
if (jarEntry.name.endsWith(".class")) {
val classNode = ClassNode()
ClassReader(jis.readAllBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
targetMethods.addAll(classNode.methods)
}
}
}
// reducing to required methods via signatures
cache.Methods = MethodResolver(targetMethods, signatures).resolve()
}
companion object {
fun loadFromFile(file: String, signatures: List<Signature>): Patcher = Patcher(File(file), signatures)
cache.methods.putAll(MethodResolver(Jar2ASM.jar2asm(input), signatures).resolve())
}
fun addPatches(vararg patches: Patch) {
this.patches.addAll(patches)
}
fun executePatches(): String? {
for (patch in patches) {
val result = patch.execute()
if (result.isSuccess()) continue
return result.error()!!.errorMessage()
fun executePatches(): Map<String, Result<Nothing?>> {
return buildMap {
for (patch in patches) {
val result: Result<Nothing?> = try {
val pr = patch.execute()
if (pr.isSuccess()) continue
Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error"))
} catch (e: Exception) {
Result.failure(e)
}
this[patch.patchName] = result
}
}
return null
}
}

View File

@ -1,7 +1,13 @@
package net.revanced.patcher.cache
import org.objectweb.asm.tree.MethodNode
data class Cache(
var Methods: Map<String, MethodNode> = mutableMapOf()
val methods: MethodMap = MethodMap()
)
class MethodMap : LinkedHashMap<String, PatchData>() {
override fun get(key: String): PatchData {
return super.get(key) ?: throw MethodNotFoundException("Method $key not found in method cache")
}
}
class MethodNotFoundException(s: String) : Exception(s)

View File

@ -0,0 +1,9 @@
package net.revanced.patcher.cache
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
data class PatchData(
val cls: ClassNode,
val method: MethodNode
)

View File

@ -0,0 +1,67 @@
package net.revanced.patcher.resolver
import net.revanced.patcher.cache.PatchData
import net.revanced.patcher.signature.Signature
import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.InsnList
import org.objectweb.asm.tree.MethodNode
internal class MethodResolver(private val classList: List<ClassNode>, private val signatures: Array<Signature>) {
fun resolve(): MutableMap<String, PatchData> {
val patchData = mutableMapOf<String, PatchData>()
for ((classNode, methods) in classList) {
for (method in methods) {
for (signature in signatures) {
if (patchData.containsKey(signature.name)) continue // method already found for this sig
if (!this.cmp(method, signature)) continue
patchData[signature.name] = PatchData(classNode, method)
}
}
}
for (signature in signatures) {
if (patchData.containsKey(signature.name)) continue
// not found
// TODO log error or whatever
}
return patchData
}
private fun cmp(method: MethodNode, signature: Signature): Boolean {
if (signature.returns != Type.getReturnType(method.desc)) return false
if (signature.accessors != method.access) return false
if (!signature.parameters.contentEquals(Type.getArgumentTypes(method.desc))) return false
val result = method.instructions.scanFor(signature.opcodes)
if (!result.found) return false
// TODO make use of the startIndex and endIndex we have from the result
return true
}
}
private operator fun ClassNode.component1(): ClassNode {
return this
}
private operator fun ClassNode.component2(): List<MethodNode> {
return this.methods
}
private fun InsnList.scanFor(pattern: Array<Int>): ScanResult {
for (i in 0 until this.size()) {
var occurrence = 0
while (i + occurrence < this.size()) {
val current = i + occurrence
if (this[current].opcode != pattern[occurrence]) break
if (++occurrence >= pattern.size) {
return ScanResult(true, current - pattern.size, current)
}
}
}
return ScanResult(false)
}

View File

@ -0,0 +1,7 @@
package net.revanced.patcher.resolver
internal data class ScanResult(
val found: Boolean,
val startIndex: Int? = 0,
val endIndex: Int? = 0
)

View File

@ -1,4 +1,4 @@
package net.revanced.patcher.signature.model
package net.revanced.patcher.signature
import org.objectweb.asm.Type
import org.objectweb.asm.tree.ParameterNode
@ -13,13 +13,14 @@ import org.objectweb.asm.tree.ParameterNode
* This method name will be used to find the corresponding patch.
* @param returns The return type/signature of the method.
* @param accessors The accessors of the method.
* @param parameters The parameter types/signatures of the method.
* @param opcodes The opcode pattern of the method, used to find the method by signature scanning.
* @param parameters The parameter types of the method.
* @param opcodes The opcode pattern of the method, used to find the method by pattern scanning.
*/
@Suppress("ArrayInDataClass")
data class Signature(
val name: String,
val returns: Type,
@Suppress("ArrayInDataClass") val accessors: Int,
@Suppress("ArrayInDataClass") val parameters: Array<ParameterNode>,
@Suppress("ArrayInDataClass") val opcodes: Array<Int>
val accessors: Int,
val parameters: Array<Type>,
val opcodes: Array<Int>
)

View File

@ -1,12 +0,0 @@
package net.revanced.patcher.signature
import com.google.gson.Gson
import net.revanced.patcher.signature.model.Signature
object SignatureLoader {
private val gson = Gson()
fun LoadFromJson(json: String): Array<Signature> {
return gson.fromJson(json, Array<Signature>::class.java)
}
}

View File

@ -0,0 +1,12 @@
package net.revanced.patcher.util
import org.objectweb.asm.Type
object ExtraTypes {
/**
* Any object type.
* Should be used instead of types such as: "Ljava/lang/String;"
*/
val Any = Type.getType(Object::class.java)
val ArrayAny = Type.getType(Array<String>::class.java)
}

View File

@ -0,0 +1,22 @@
package net.revanced.patcher.util
import org.objectweb.asm.ClassReader
import org.objectweb.asm.tree.ClassNode
import java.io.InputStream
import java.util.jar.JarInputStream
object Jar2ASM {
fun jar2asm(input: InputStream): List<ClassNode> {
return buildList {
val jar = JarInputStream(input)
while (true) {
val e = jar.nextJarEntry ?: break
if (e.name.endsWith(".class")) {
val classNode = ClassNode()
ClassReader(jar.readAllBytes()).accept(classNode, ClassReader.EXPAND_FRAMES)
this.add(classNode)
}
}
}
}
}

View File

@ -1,11 +0,0 @@
package net.revanced.patcher.util
import org.objectweb.asm.tree.ClassNode
class PatternScanner (private val classes: Array<ClassNode>) {
companion object {
fun scan(){
}
}
}

View File

@ -1,30 +1,47 @@
package net.revanced.patcher
import net.revanced.patcher.patch.Patch
import net.revanced.patcher.patch.PatchResultError
import net.revanced.patcher.patch.PatchResultSuccess
import net.revanced.patcher.signature.SignatureLoader
import net.revanced.patcher.signature.Signature
import net.revanced.patcher.util.ExtraTypes
import org.junit.jupiter.api.Test
import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.Type
internal class PatcherTest {
@Test
fun template() {
val patcher = Patcher.loadFromFile(
"some.apk",
SignatureLoader.LoadFromJson("signatures.json").toMutableList()
private val testSigs: Array<Signature> = arrayOf(
Signature(
"testMethod",
Type.BOOLEAN_TYPE,
ACC_PUBLIC or ACC_STATIC,
arrayOf(
ExtraTypes.ArrayAny,
),
arrayOf(
GETSTATIC,
LDC,
INVOKEVIRTUAL
)
)
)
val patches = mutableListOf(
Patch ("RemoveVideoAds") {
val videoAdShowMethodInstr = patcher.cache.Methods["SomeMethod"]?.instructions
@Test
fun testPatcher() {
val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!!
val patcher = Patcher(testData, testSigs)
patcher.addPatches(
Patch ("TestPatch") {
patcher.cache.methods["testMethod"]
PatchResultSuccess()
},
Patch ("TweakLayout") {
val layoutMethod = patcher.cache.Methods["SomeMethod2"]
PatchResultError("Failed")
}
)
patcher.executePatches()
val result = patcher.executePatches()
for ((s, r) in result) {
if (r.isFailure) {
throw Exception("Patch $s failed", r.exceptionOrNull()!!)
}
}
}
}