From 36519811610192e299834e9d00627a94faad56a9 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 30 Mar 2022 15:10:18 +0200 Subject: [PATCH] feat: migrate to dexlib BREAKING CHANGE: Removed usage of ASM library --- build.gradle.kts | 5 +- .../kotlin/app/revanced/patcher/Patcher.kt | 74 ++++---- .../app/revanced/patcher/cache/Cache.kt | 28 ++- .../app/revanced/patcher/cache/PatchData.kt | 22 --- .../patcher/cache/proxy/ClassProxy.kt | 21 +++ .../proxy/mutableTypes/MutableAnnotation.kt | 29 +++ .../mutableTypes/MutableAnnotationElement.kt | 33 ++++ .../cache/proxy/mutableTypes/MutableClass.kt | 94 ++++++++++ .../proxy/mutableTypes/MutableEncodedValue.kt | 26 +++ .../cache/proxy/mutableTypes/MutableField.kt | 65 +++++++ .../cache/proxy/mutableTypes/MutableMethod.kt | 58 ++++++ .../mutableTypes/MutableMethodParameter.kt | 35 ++++ .../app/revanced/patcher/patch/Patch.kt | 2 +- .../patcher/resolver/MethodResolver.kt | 137 ++++++-------- ...nResult.kt => MethodResolverScanResult.kt} | 4 +- .../patcher/signature/MethodSignature.kt | 12 ++ .../signature/MethodSignatureScanResult.kt | 23 +++ .../revanced/patcher/signature/Signature.kt | 27 --- .../app/revanced/patcher/util/ExtraTypes.kt | 12 -- .../kotlin/app/revanced/patcher/util/Io.kt | 94 ---------- .../app/revanced/patcher/writer/ASMWriter.kt | 21 --- .../app/revanced/patcher/PatcherTest.kt | 177 ------------------ .../kotlin/app/revanced/patcher/ReaderTest.kt | 12 -- .../app/revanced/patcher/util/TestUtil.kt | 45 ----- src/test/kotlin/patcher/PatcherTest.kt | 75 ++++++++ 25 files changed, 590 insertions(+), 541 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/patcher/cache/PatchData.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/ClassProxy.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotation.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotationElement.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableClass.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableEncodedValue.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableField.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethod.kt create mode 100644 src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethodParameter.kt rename src/main/kotlin/app/revanced/patcher/resolver/{ScanResult.kt => MethodResolverScanResult.kt} (71%) create mode 100644 src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt create mode 100644 src/main/kotlin/app/revanced/patcher/signature/MethodSignatureScanResult.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/signature/Signature.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/util/ExtraTypes.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/util/Io.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/writer/ASMWriter.kt delete mode 100644 src/test/kotlin/app/revanced/patcher/PatcherTest.kt delete mode 100644 src/test/kotlin/app/revanced/patcher/ReaderTest.kt delete mode 100644 src/test/kotlin/app/revanced/patcher/util/TestUtil.kt create mode 100644 src/test/kotlin/patcher/PatcherTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index f295c2d..6d8e562 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,10 +12,7 @@ repositories { dependencies { implementation(kotlin("stdlib")) - implementation("org.ow2.asm:asm:9.2") - 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.github.lanchon.dexpatcher:multidexlib2:2.3.4") implementation("io.github.microutils:kotlin-logging:2.1.21") testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger! testImplementation(kotlin("test")) diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index 618dbdd..4580276 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -3,49 +3,51 @@ package app.revanced.patcher import app.revanced.patcher.cache.Cache import app.revanced.patcher.patch.Patch import app.revanced.patcher.resolver.MethodResolver -import app.revanced.patcher.signature.Signature -import app.revanced.patcher.util.Io -import org.objectweb.asm.tree.ClassNode -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream +import app.revanced.patcher.signature.MethodSignature +import lanchon.multidexlib2.BasicDexFileNamer +import lanchon.multidexlib2.MultiDexIO +import org.jf.dexlib2.Opcodes +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.iface.DexFile +import java.io.File -/** - * The Patcher class. - * ***It is of utmost importance that the input and output streams are NEVER closed.*** - * - * @param input the input stream to read from, must be a JAR - * @param output the output stream to write to - * @param signatures the signatures - * @sample app.revanced.patcher.PatcherTest - * @throws IOException if one of the streams are closed - */ class Patcher( - private val input: InputStream, - private val output: OutputStream, - signatures: Array, -) { - var cache: Cache + input: File, + private val output: File, + signatures: Array, - private var io: Io - private val patches = mutableListOf() + ) { + private val cache: Cache + private val patches = mutableSetOf() init { - val classes = mutableListOf() - io = Io(input, output, classes) - io.readFromJar() - cache = Cache(classes, MethodResolver(classes, signatures).resolve()) + // TODO: find a way to load all dex classes, the code below only loads the first .dex file + val dexFile = MultiDexIO.readDexFile(true, input, BasicDexFileNamer(), Opcodes.getDefault(), null) + cache = Cache(dexFile.classes, MethodResolver(dexFile.classes, signatures).resolve()) } - /** - * Saves the output to the output stream. - * Calling this method will close the input and output streams, - * meaning this method should NEVER be called after. - * - * @throws IOException if one of the streams are closed - */ fun save() { - io.saveAsJar() + val newDexFile = object : DexFile { + override fun getClasses(): MutableSet { + // TODO: find a way to return a set with a custom iterator + // TODO: the iterator would return the proxied class matching the current index of the list + // TODO: instead of the original class + for (classProxy in cache.classProxy) { + if (!classProxy.proxyused) continue + // TODO: merge this class with cache.classes somehow in an iterator + classProxy.mutatedClass + } + return cache.classes.toMutableSet() + } + + override fun getOpcodes(): Opcodes { + // TODO find a way to get the opcodes format + return Opcodes.getDefault() + } + } + + // TODO: not sure about maxDexPoolSize & we should use the multithreading overload for writeDexFile + MultiDexIO.writeDexFile(true, output, BasicDexFileNamer(), newDexFile, 10, null) } fun addPatches(vararg patches: Patch) { @@ -67,4 +69,4 @@ class Patcher( } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/app/revanced/patcher/cache/Cache.kt b/src/main/kotlin/app/revanced/patcher/cache/Cache.kt index 3a1d1a9..b660191 100644 --- a/src/main/kotlin/app/revanced/patcher/cache/Cache.kt +++ b/src/main/kotlin/app/revanced/patcher/cache/Cache.kt @@ -1,14 +1,30 @@ package app.revanced.patcher.cache -import org.objectweb.asm.tree.ClassNode +import app.revanced.patcher.cache.proxy.ClassProxy +import app.revanced.patcher.signature.MethodSignatureScanResult +import org.jf.dexlib2.iface.ClassDef class Cache( - val classes: List, - val methods: MethodMap -) + internal val classes: Set, + val resolvedMethods: MethodMap +) { + internal val classProxy = mutableListOf() -class MethodMap : LinkedHashMap() { - override fun get(key: String): PatchData { + fun findClass(predicate: (ClassDef) -> Boolean): ClassProxy? { + // if a class has been found with the given predicate, + val foundClass = classes.singleOrNull(predicate) ?: return null + // create a class proxy with the index of the class in the classes list + // TODO: There might be a more elegant way to the comment above + val classProxy = ClassProxy(foundClass, classes.indexOf(foundClass)) + // add it to the cache and + this.classProxy.add(classProxy) + // return the proxy class + return classProxy + } +} + +class MethodMap : LinkedHashMap() { + override fun get(key: String): MethodSignatureScanResult { return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache") } } diff --git a/src/main/kotlin/app/revanced/patcher/cache/PatchData.kt b/src/main/kotlin/app/revanced/patcher/cache/PatchData.kt deleted file mode 100644 index 033e497..0000000 --- a/src/main/kotlin/app/revanced/patcher/cache/PatchData.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.revanced.patcher.cache - -import app.revanced.patcher.resolver.MethodResolver -import app.revanced.patcher.signature.Signature -import org.objectweb.asm.tree.ClassNode -import org.objectweb.asm.tree.MethodNode - -data class PatchData( - val declaringClass: ClassNode, - val method: MethodNode, - val scanData: PatternScanData -) { - @Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method. - fun findParentMethod(signature: Signature): PatchData? { - return MethodResolver.resolveMethod(declaringClass, signature) - } -} - -data class PatternScanData( - val startIndex: Int, - val endIndex: Int -) diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/ClassProxy.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/ClassProxy.kt new file mode 100644 index 0000000..79d2ab1 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/ClassProxy.kt @@ -0,0 +1,21 @@ +package app.revanced.patcher.cache.proxy + +import app.revanced.patcher.cache.proxy.mutableTypes.MutableClass +import org.jf.dexlib2.iface.ClassDef + + +class ClassProxy( + val immutableClass: ClassDef, + val originalClassIndex: Int, +) { + internal var proxyused = false + internal lateinit var mutatedClass: MutableClass + + fun resolve(): MutableClass { + if (!proxyused) { + proxyused = true + mutatedClass = MutableClass(immutableClass) + } + return mutatedClass + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotation.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotation.kt new file mode 100644 index 0000000..1a73b70 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotation.kt @@ -0,0 +1,29 @@ +package app.revanced.patcher.cache.proxy.mutableTypes + +import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable +import org.jf.dexlib2.base.BaseAnnotation +import org.jf.dexlib2.iface.Annotation + +class MutableAnnotation(annotation: Annotation) : BaseAnnotation() { + private val visibility = annotation.visibility + private val type = annotation.type + private val elements = annotation.elements.map { element -> element.toMutable() }.toMutableSet() + + override fun getType(): String { + return type + } + + override fun getElements(): MutableSet { + return elements + } + + override fun getVisibility(): Int { + return visibility + } + + companion object { + fun Annotation.toMutable(): MutableAnnotation { + return MutableAnnotation(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotationElement.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotationElement.kt new file mode 100644 index 0000000..d587ba2 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotationElement.kt @@ -0,0 +1,33 @@ +package app.revanced.patcher.cache.proxy.mutableTypes + +import app.revanced.patcher.cache.proxy.mutableTypes.MutableEncodedValue.Companion.toMutable +import org.jf.dexlib2.base.BaseAnnotationElement +import org.jf.dexlib2.iface.AnnotationElement +import org.jf.dexlib2.iface.value.EncodedValue + +class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() { + private var name = annotationElement.name + private var value = annotationElement.value.toMutable() + + fun setName(name: String) { + this.name = name + } + + fun setValue(value: MutableEncodedValue) { + this.value = value + } + + override fun getName(): String { + return name + } + + override fun getValue(): EncodedValue { + return value + } + + companion object { + fun AnnotationElement.toMutable(): MutableAnnotationElement { + return MutableAnnotationElement(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableClass.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableClass.kt new file mode 100644 index 0000000..7e33fad --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableClass.kt @@ -0,0 +1,94 @@ +package app.revanced.patcher.cache.proxy.mutableTypes + +import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import app.revanced.patcher.cache.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.cache.proxy.mutableTypes.MutableMethod.Companion.toMutable +import org.jf.dexlib2.base.reference.BaseTypeReference +import org.jf.dexlib2.iface.ClassDef + +class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() { + // Class + private var type = classDef.type + private var sourceFile = classDef.sourceFile + private var accessFlags = classDef.accessFlags + private var superclass = classDef.superclass + + private val interfaces = classDef.interfaces.toMutableList() + private val annotations = classDef.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() + + // Methods + private val methods = classDef.methods.map { method -> method.toMutable() }.toMutableSet() + private val directMethods = classDef.directMethods.map { directMethod -> directMethod.toMutable() }.toMutableSet() + private val virtualMethods = + classDef.virtualMethods.map { virtualMethod -> virtualMethod.toMutable() }.toMutableSet() + + // Fields + private val fields = classDef.fields.map { field -> field.toMutable() }.toMutableSet() + private val staticFields = classDef.staticFields.map { staticField -> staticField.toMutable() }.toMutableSet() + private val instanceFields = + classDef.instanceFields.map { instanceFields -> instanceFields.toMutable() }.toMutableSet() + + fun setType(type: String) { + this.type = type + } + + fun setSourceFile(sourceFile: String?) { + this.sourceFile = sourceFile + } + + fun setAccessFlags(accessFlags: Int) { + this.accessFlags = accessFlags + } + + fun setSuperClass(superclass: String?) { + this.superclass = superclass + } + + override fun getType(): String { + return type + } + + override fun getAccessFlags(): Int { + return accessFlags + } + + override fun getSourceFile(): String? { + return sourceFile + } + + override fun getSuperclass(): String? { + return superclass + } + + override fun getInterfaces(): MutableList { + return interfaces + } + + override fun getAnnotations(): MutableSet { + return annotations + } + + override fun getStaticFields(): MutableSet { + return staticFields + } + + override fun getInstanceFields(): MutableSet { + return instanceFields + } + + override fun getFields(): MutableSet { + return fields + } + + override fun getDirectMethods(): MutableSet { + return directMethods + } + + override fun getVirtualMethods(): MutableSet { + return virtualMethods + } + + override fun getMethods(): MutableSet { + return methods + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableEncodedValue.kt new file mode 100644 index 0000000..52cba9c --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableEncodedValue.kt @@ -0,0 +1,26 @@ +package app.revanced.patcher.cache.proxy.mutableTypes + +import org.jf.dexlib2.iface.value.EncodedValue + +class MutableEncodedValue(encodedValue: EncodedValue) : EncodedValue { + private var valueType = encodedValue.valueType + + fun setValueType(valueType: Int) { + this.valueType = valueType + } + + override fun compareTo(other: EncodedValue): Int { + return valueType - other.valueType + + } + + override fun getValueType(): Int { + return valueType + } + + companion object { + fun EncodedValue.toMutable(): MutableEncodedValue { + return MutableEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableField.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableField.kt new file mode 100644 index 0000000..66c75e3 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableField.kt @@ -0,0 +1,65 @@ +package app.revanced.patcher.cache.proxy.mutableTypes + +import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import app.revanced.patcher.cache.proxy.mutableTypes.MutableEncodedValue.Companion.toMutable +import org.jf.dexlib2.base.reference.BaseFieldReference +import org.jf.dexlib2.iface.Field + +class MutableField(field: Field) : Field, BaseFieldReference() { + private var definingClass = field.definingClass + private var name = field.name + private var type = field.type + private var accessFlags = field.accessFlags + private var initialValue = field.initialValue?.toMutable() + private val annotations = field.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() + + fun setDefiningClass(definingClass: String) { + this.definingClass + } + + fun setName(name: String) { + this.name = name + } + + fun setType(type: String) { + this.type = type + } + + fun setAccessFlags(accessFlags: Int) { + this.accessFlags = accessFlags + } + + fun setInitialValue(initialValue: MutableEncodedValue?) { + this.initialValue = initialValue + } + + override fun getDefiningClass(): String { + return this.definingClass + } + + override fun getName(): String { + return this.name + } + + override fun getType(): String { + return this.type + } + + override fun getAnnotations(): MutableSet { + return this.annotations + } + + override fun getAccessFlags(): Int { + return this.accessFlags + } + + override fun getInitialValue(): MutableEncodedValue? { + return this.initialValue + } + + companion object { + fun Field.toMutable(): MutableField { + return MutableField(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethod.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethod.kt new file mode 100644 index 0000000..e21ce80 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethod.kt @@ -0,0 +1,58 @@ +package app.revanced.patcher.cache.proxy.mutableTypes + +import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import app.revanced.patcher.cache.proxy.mutableTypes.MutableMethodParameter.Companion.toMutable +import org.jf.dexlib2.base.reference.BaseMethodReference +import org.jf.dexlib2.builder.MutableMethodImplementation +import org.jf.dexlib2.iface.Method + +class MutableMethod(method: Method) : Method, BaseMethodReference() { + private var definingClass = method.definingClass + private var name = method.name + private var accessFlags = method.accessFlags + private var returnType = method.returnType + + // Create own mutable MethodImplementation (due to not being able to change members like register count) + private var implementation = method.implementation?.let { MutableMethodImplementation(it) } + private val annotations = method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() + private val parameters = method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() + private val parameterTypes = method.parameterTypes.toMutableList() + + override fun getDefiningClass(): String { + return this.definingClass + } + + override fun getName(): String { + return name + } + + override fun getParameterTypes(): MutableList { + return parameterTypes + } + + override fun getReturnType(): String { + return returnType + } + + override fun getAnnotations(): MutableSet { + return annotations + } + + override fun getAccessFlags(): Int { + return accessFlags + } + + override fun getParameters(): MutableList { + return parameters + } + + override fun getImplementation(): MutableMethodImplementation? { + return implementation + } + + companion object { + fun Method.toMutable(): MutableMethod { + return MutableMethod(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethodParameter.kt b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethodParameter.kt new file mode 100644 index 0000000..023d9f9 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethodParameter.kt @@ -0,0 +1,35 @@ +package app.revanced.patcher.cache.proxy.mutableTypes + +import app.revanced.patcher.cache.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import org.jf.dexlib2.base.BaseMethodParameter +import org.jf.dexlib2.iface.MethodParameter + +// TODO: finish overriding all members if necessary +class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() { + private var type = parameter.type + private var name = parameter.name + private var signature = parameter.signature + private val annotations = parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() + + override fun getType(): String { + return type + } + + override fun getName(): String? { + return name + } + + override fun getSignature(): String? { + return signature + } + + override fun getAnnotations(): MutableSet { + return annotations + } + + companion object { + fun MethodParameter.toMutable(): MutableMethodParameter { + return MutableMethodParameter(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index e2e14ac..4b35ae4 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -4,4 +4,4 @@ import app.revanced.patcher.cache.Cache abstract class Patch(val patchName: String) { abstract fun execute(cache: Cache): PatchResult -} +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt b/src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt index ca0a41a..ecc1898 100644 --- a/src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt +++ b/src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt @@ -1,36 +1,31 @@ package app.revanced.patcher.resolver import app.revanced.patcher.cache.MethodMap -import app.revanced.patcher.cache.PatchData -import app.revanced.patcher.cache.PatternScanData -import app.revanced.patcher.signature.Signature -import app.revanced.patcher.util.ExtraTypes -import mu.KotlinLogging -import org.objectweb.asm.Type -import org.objectweb.asm.tree.* +import app.revanced.patcher.signature.MethodSignatureScanResult +import app.revanced.patcher.signature.PatternScanData +import app.revanced.patcher.signature.MethodSignature +import org.jf.dexlib2.Opcode +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.iface.Method -private val logger = KotlinLogging.logger("MethodResolver") - -internal class MethodResolver(private val classList: List, private val signatures: Array) { +// TODO: add logger +internal class MethodResolver(private val classes: Set, private val signatures: Array) { fun resolve(): MethodMap { val methodMap = MethodMap() - for ((classNode, methods) in classList) { - for (method in methods) { - for (signature in signatures) { - if (methodMap.containsKey(signature.name)) { // method already found for this sig - logger.trace { "Sig ${signature.name} already found, skipping." } + for (classDef in classes) { + for (method in classDef.methods) { + for (methodSignature in signatures) { + if (methodMap.containsKey(methodSignature.name)) { // method already found for this sig continue } - logger.trace { "Resolving sig ${signature.name}: ${classNode.name} / ${method.name}" } - val (r, sr) = cmp(method, signature) + + val (r, sr) = cmp(method, methodSignature) if (!r || sr == null) { - logger.trace { "Compare result for sig ${signature.name} has failed!" } continue } - logger.trace { "Method for sig ${signature.name} found!" } - methodMap[signature.name] = PatchData( - classNode, + + methodMap[methodSignature.name] = MethodSignatureScanResult( method, PatternScanData( // sadly we cannot create contracts for a data class, so we must assert @@ -44,7 +39,6 @@ internal class MethodResolver(private val classList: List, private va for (signature in signatures) { if (methodMap.containsKey(signature.name)) continue - logger.error { "Could not find method for sig ${signature.name}!" } } return methodMap @@ -52,12 +46,11 @@ internal class MethodResolver(private val classList: List, private va // These functions do not require the constructor values, so they can be static. companion object { - fun resolveMethod(classNode: ClassNode, signature: Signature): PatchData? { + fun resolveMethod(classNode: ClassDef, signature: MethodSignature): MethodSignatureScanResult? { for (method in classNode.methods) { val (r, sr) = cmp(method, signature) if (!r || sr == null) continue - return PatchData( - classNode, + return MethodSignatureScanResult( method, PatternScanData(0, 0) // opcode list is always ignored. ) @@ -65,92 +58,72 @@ internal class MethodResolver(private val classList: List, private va return null } - private fun cmp(method: MethodNode, signature: Signature): Pair { - signature.returns?.let { _ -> - val methodReturns = Type.getReturnType(method.desc).convertObject() - if (signature.returns != methodReturns) { - logger.trace { - """ - Comparing sig ${signature.name}: invalid return type: - expected ${signature.returns}, - got $methodReturns - """.trimIndent() - } + private fun cmp(method: Method, signature: MethodSignature): Pair { + // TODO: compare as generic object if not primitive + signature.returnType?.let { _ -> + if (signature.returnType != method.returnType) { return@cmp false to null } } - signature.accessors?.let { _ -> - if (signature.accessors != method.access) { - logger.trace { - """ - Comparing sig ${signature.name}: invalid accessors: - expected ${signature.accessors}, - got ${method.access} - """.trimIndent() - } + signature.accessFlags?.let { _ -> + if (signature.accessFlags != method.accessFlags) { return@cmp false to null } } - signature.parameters?.let { _ -> - val parameters = Type.getArgumentTypes(method.desc).convertObjects() - if (!signature.parameters.contentEquals(parameters)) { - logger.trace { - """ - Comparing sig ${signature.name}: invalid parameter types: - expected ${signature.parameters.joinToString()}}, - got ${parameters.joinToString()} - """.trimIndent() - } + // TODO: compare as generic object if the parameter is not primitive + signature.methodParameters?.let { _ -> + if (signature.methodParameters != method.parameters) { return@cmp false to null } } signature.opcodes?.let { _ -> - val result = method.instructions.scanFor(signature.opcodes) - if (!result.found) { - logger.trace { "Comparing sig ${signature.name}: invalid opcode pattern" } - return@cmp false to null - } - return@cmp true to result + val result = method.implementation?.instructions?.scanFor(signature.opcodes) + return@cmp if (result != null && result.found) true to result else false to null } - return true to ScanResult(true) + return true to MethodResolverScanResult(true) } } } -private operator fun ClassNode.component1() = this -private operator fun ClassNode.component2() = this.methods +private operator fun ClassDef.component1() = this +private operator fun ClassDef.component2() = this.methods -private fun InsnList.scanFor(pattern: IntArray): ScanResult { - for (i in 0 until this.size()) { +private fun MutableIterable.scanFor(pattern: Array): MethodResolverScanResult { + // TODO: create var for count? + for (i in 0 until this.count()) { var occurrence = 0 - while (i + occurrence < this.size()) { - val n = this[i + occurrence] - if (!n.shouldSkip() && n.opcode != pattern[occurrence]) break + while (i + occurrence < this.count()) { + val n = this.elementAt(i + occurrence) + if (!n.shouldSkip() && n != pattern[occurrence]) break if (++occurrence >= pattern.size) { val current = i + occurrence - return ScanResult(true, current - pattern.size, current) + return MethodResolverScanResult(true, current - pattern.size, current) } } } - return ScanResult(false) + return MethodResolverScanResult(false) } -private fun Type.convertObject(): Type { - return when (this.sort) { - Type.OBJECT -> ExtraTypes.Any - Type.ARRAY -> ExtraTypes.ArrayAny - else -> this - } +// TODO: extend Opcode type, not T (requires a cast to Opcode) +private fun T.shouldSkip(): Boolean { + return this == Opcode.GOTO // TODO: and: this == AbstractInsnNode.LINE } -private fun Array.convertObjects(): Array { - return this.map { it.convertObject() }.toTypedArray() -} +// TODO: use this somehow to compare types as generic objects if not primitive +// private fun Type.convertObject(): Type { +// return when (this.sort) { +// Type.OBJECT -> ExtraTypes.Any +// Type.ARRAY -> ExtraTypes.ArrayAny +// else -> this +// } +// } +// +// private fun Array.convertObjects(): Array { +// return this.map { it.convertObject() }.toTypedArray() +// } -private fun AbstractInsnNode.shouldSkip() = - type == AbstractInsnNode.LABEL || type == AbstractInsnNode.LINE diff --git a/src/main/kotlin/app/revanced/patcher/resolver/ScanResult.kt b/src/main/kotlin/app/revanced/patcher/resolver/MethodResolverScanResult.kt similarity index 71% rename from src/main/kotlin/app/revanced/patcher/resolver/ScanResult.kt rename to src/main/kotlin/app/revanced/patcher/resolver/MethodResolverScanResult.kt index 135b0ca..1d7c4ba 100644 --- a/src/main/kotlin/app/revanced/patcher/resolver/ScanResult.kt +++ b/src/main/kotlin/app/revanced/patcher/resolver/MethodResolverScanResult.kt @@ -1,7 +1,7 @@ package app.revanced.patcher.resolver -internal data class ScanResult( +internal data class MethodResolverScanResult( val found: Boolean, val startIndex: Int? = 0, val endIndex: Int? = 0 -) +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt new file mode 100644 index 0000000..6b870dc --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt @@ -0,0 +1,12 @@ +package app.revanced.patcher.signature + +import org.jf.dexlib2.Opcode + +@Suppress("ArrayInDataClass") +data class MethodSignature( + val name: String, + val returnType: String?, + val accessFlags: Int?, + val methodParameters: Iterable?, + val opcodes: Array? +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/signature/MethodSignatureScanResult.kt b/src/main/kotlin/app/revanced/patcher/signature/MethodSignatureScanResult.kt new file mode 100644 index 0000000..5f2927a --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/signature/MethodSignatureScanResult.kt @@ -0,0 +1,23 @@ +package app.revanced.patcher.signature + +import app.revanced.patcher.resolver.MethodResolver +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.iface.Method +import org.jf.dexlib2.immutable.reference.ImmutableTypeReference + +// TODO: IMPORTANT: we might have to use a class proxy as well here +data class MethodSignatureScanResult( + val method: Method, + val scanData: PatternScanData +) { + @Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method. + fun findParentMethod(signature: MethodSignature): MethodSignatureScanResult? { + // TODO: find a way to get the classNode out of method.definingClass + return MethodResolver.resolveMethod(ImmutableTypeReference(method.definingClass) as ClassDef, signature) + } +} + +data class PatternScanData( + val startIndex: Int, + val endIndex: Int +) diff --git a/src/main/kotlin/app/revanced/patcher/signature/Signature.kt b/src/main/kotlin/app/revanced/patcher/signature/Signature.kt deleted file mode 100644 index 19f303d..0000000 --- a/src/main/kotlin/app/revanced/patcher/signature/Signature.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.revanced.patcher.signature - -import org.objectweb.asm.Type - -/** - * An ASM signature list for the Patcher. - * - * @param name The name of the method. - * Do not use the actual method name, instead try to guess what the method name originally was. - * If you are unable to guess a method name, doing something like "patch-name-1" is fine too. - * For example: "override-codec-1". - * This method name will be mapped to the method matching the signature. - * Even though this is technically not needed for the `findParentMethod` method, - * it is still recommended giving the method a name, so it can be identified easily. - * @param returns The return type/signature of the method. - * @param accessors The accessors of the method. - * @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?, - val accessors: Int?, - val parameters: Array?, - val opcodes: IntArray? -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ExtraTypes.kt b/src/main/kotlin/app/revanced/patcher/util/ExtraTypes.kt deleted file mode 100644 index 430a4be..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/ExtraTypes.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.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 = Type.getType(Object::class.java) - val ArrayAny: Type = Type.getType(Array::class.java) -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/Io.kt b/src/main/kotlin/app/revanced/patcher/util/Io.kt deleted file mode 100644 index 4db1a27..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/Io.kt +++ /dev/null @@ -1,94 +0,0 @@ -package app.revanced.patcher.util - -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassWriter -import org.objectweb.asm.tree.ClassNode -import java.io.BufferedInputStream -import java.io.InputStream -import java.io.OutputStream -import java.util.jar.JarEntry -import java.util.jar.JarInputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream - -internal class Io( - private val input: InputStream, - private val output: OutputStream, - private val classes: MutableList -) { - private val bufferedInputStream = BufferedInputStream(input) - - fun readFromJar() { - bufferedInputStream.mark(Integer.MAX_VALUE) - // create a BufferedInputStream in order to read the input stream again when calling saveAsJar(..) - val jis = JarInputStream(bufferedInputStream) - - // read all entries from the input stream - // we use JarEntry because we only read .class files - lateinit var jarEntry: JarEntry - while (jis.nextJarEntry.also { if (it != null) jarEntry = it } != null) { - // if the current entry ends with .class (indicating a java class file), add it to our list of classes to return - if (jarEntry.name.endsWith(".class")) { - // create a new ClassNode - val classNode = ClassNode() - // read the bytes with a ClassReader into the ClassNode - ClassReader(jis.readBytes()).accept(classNode, ClassReader.EXPAND_FRAMES) - // add it to our list - classes.add(classNode) - } - - // finally, close the entry - jis.closeEntry() - } - - // at last reset the buffered input stream - bufferedInputStream.reset() - } - - fun saveAsJar() { - val jis = ZipInputStream(bufferedInputStream) - val jos = ZipOutputStream(output) - val classReaders = mutableMapOf() - - // first write all non .class zip entries from the original input stream to the output stream - // we read it first to close the input stream as fast as possible - // TODO(oSumAtrIX): There is currently no way to remove non .class files. - lateinit var zipEntry: ZipEntry - while (jis.nextEntry.also { if (it != null) zipEntry = it } != null) { - if (zipEntry.name.endsWith(".class")) { - classReaders[zipEntry.name] = ClassReader(jis.readBytes()) - continue - } - - // create a new zipEntry and write the contents of the zipEntry to the output stream and close it - jos.putNextEntry(ZipEntry(zipEntry)) - jos.write(jis.readBytes()) - jos.closeEntry() - } - - // finally, close the input stream - jis.close() - bufferedInputStream.close() - input.close() - - // now write all the patched classes to the output stream - for (patchedClass in classes) { - // create a new entry of the patched class - val name = patchedClass.name + ".class" - jos.putNextEntry(JarEntry(name)) - - // parse the patched class to a byte array and write it to the output stream - val cw = ClassWriter(classReaders[name]!!, ClassWriter.COMPUTE_MAXS) - patchedClass.accept(cw) - jos.write(cw.toByteArray()) - - // close the newly created jar entry - jos.closeEntry() - } - - // finally, close the rest of the streams - jos.close() - output.close() - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/writer/ASMWriter.kt b/src/main/kotlin/app/revanced/patcher/writer/ASMWriter.kt deleted file mode 100644 index 1a8a014..0000000 --- a/src/main/kotlin/app/revanced/patcher/writer/ASMWriter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patcher.writer - -import org.objectweb.asm.tree.AbstractInsnNode -import org.objectweb.asm.tree.InsnList - -object ASMWriter { - fun InsnList.setAt(index: Int, node: AbstractInsnNode) { - this[this.get(index)] = node - } - - fun InsnList.insertAt(index: Int = 0, vararg nodes: AbstractInsnNode) { - this.insert(this.get(index), nodes.toInsnList()) - } - - // TODO(Sculas): Should this be public? - private fun Array.toInsnList(): InsnList { - val list = InsnList() - this.forEach { list.add(it) } - return list - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt deleted file mode 100644 index 94824da..0000000 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ /dev/null @@ -1,177 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.cache.Cache -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultSuccess -import app.revanced.patcher.signature.Signature -import app.revanced.patcher.util.ExtraTypes -import app.revanced.patcher.util.TestUtil -import app.revanced.patcher.writer.ASMWriter.insertAt -import app.revanced.patcher.writer.ASMWriter.setAt -import org.junit.jupiter.api.assertDoesNotThrow -import org.objectweb.asm.Opcodes.* -import org.objectweb.asm.Type -import org.objectweb.asm.tree.FieldInsnNode -import org.objectweb.asm.tree.LdcInsnNode -import org.objectweb.asm.tree.MethodInsnNode -import java.io.ByteArrayOutputStream -import java.io.PrintStream -import kotlin.test.Test - -internal class PatcherTest { - companion object { - val testSignatures: Array = arrayOf( - // Java: - // public static void main(String[] args) { - // System.out.println("Hello, world!"); - // } - // Bytecode: - // public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V - // getstatic java/lang/System.out:java.io.PrintStream - // ldc "Hello, world!" (java.lang.String) - // invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V - // return - // } - Signature( - "mainMethod", - Type.VOID_TYPE, - ACC_PUBLIC or ACC_STATIC, - arrayOf(ExtraTypes.ArrayAny), - intArrayOf( - GETSTATIC, - LDC, - INVOKEVIRTUAL, - RETURN - ) - ) - ) - } - - @Test - fun testPatcher() { - val patcher = Patcher( - PatcherTest::class.java.getResourceAsStream("/test1.jar")!!, - ByteArrayOutputStream(), - testSignatures - ) - - patcher.addPatches( - object : Patch("TestPatch") { - override fun execute(cache: Cache): PatchResult { - // Get the method from the resolver cache - val mainMethod = patcher.cache.methods["mainMethod"] - // Get the instruction list - val instructions = mainMethod.method.instructions!! - - // Let's modify it, so it prints "Hello, ReVanced! Editing bytecode." - // Get the start index of our opcode pattern. - // This will be the index of the LDC instruction. - val startIndex = mainMethod.scanData.startIndex - - // Ignore this, just testing if the method resolver works :) - TestUtil.assertNodeEqual( - FieldInsnNode( - GETSTATIC, - Type.getInternalName(System::class.java), - "out", - // for whatever reason, it adds an "L" and ";" to the node string - "L${Type.getInternalName(PrintStream::class.java)};" - ), - instructions[startIndex]!! - ) - - // Create a new LDC node and replace the LDC instruction. - val stringNode = LdcInsnNode("Hello, ReVanced! Editing bytecode.") - instructions.setAt(startIndex, stringNode) - - // Now lets print our string twice! - // Insert our instructions after the second instruction by our pattern. - // This will place our instructions after the original INVOKEVIRTUAL call. - // You could also copy the instructions from the list and then modify the LDC instruction again, - // but this is to show a more advanced example of writing bytecode using the patcher and ASM. - instructions.insertAt( - startIndex + 1, - FieldInsnNode( - GETSTATIC, - Type.getInternalName(System::class.java), // "java/lang/System" - "out", - Type.getInternalName(PrintStream::class.java) // "java/io/PrintStream" - ), - LdcInsnNode("Hello, ReVanced! Adding bytecode."), - MethodInsnNode( - INVOKEVIRTUAL, - Type.getInternalName(PrintStream::class.java), // "java/io/PrintStream" - "println", - Type.getMethodDescriptor( - Type.VOID_TYPE, - Type.getType(String::class.java) - ) // "(Ljava/lang/String;)V" - ) - ) - - // Our code now looks like this: - // public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V - // getstatic java/lang/System.out:java.io.PrintStream - // ldc "Hello, ReVanced! Editing bytecode." (java.lang.String) // We overwrote this instruction. - // invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V - // getstatic java/lang/System.out:java.io.PrintStream // This instruction and the 2 instructions below are written manually. - // ldc "Hello, ReVanced! Adding bytecode." (java.lang.String) - // invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V - // return - // } - - // Finally, tell the patcher that this patch was a success. - // You can also return PatchResultError with a message. - // If an exception is thrown inside this function, - // a PatchResultError will be returned with the error message. - return PatchResultSuccess() - } - } - ) - - // Apply all patches loaded in the patcher - val patchResult = patcher.applyPatches() - // You can check if an error occurred - for ((patchName, result) in patchResult) { - if (result.isFailure) { - throw Exception("Patch $patchName failed", result.exceptionOrNull()!!) - } - } - - patcher.save() - } - - @Test - fun `test patcher with no changes`() { - val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!! - // val available = testData.available() - val out = ByteArrayOutputStream() - Patcher(testData, out, testSignatures).save() - // FIXME(Sculas): There seems to be a 1-byte difference, not sure what it is. - // assertEquals(available, out.size()) - out.close() - } - - @Test() - fun `should not raise an exception if any signature member except the name is missing`() { - val sigName = "testMethod" - - assertDoesNotThrow( - "Should not raise an exception if any signature member except the name is missing" - ) { - Patcher( - PatcherTest::class.java.getResourceAsStream("/test1.jar")!!, - ByteArrayOutputStream(), - arrayOf( - Signature( - sigName, - null, - null, - null, - null - )) - ) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/ReaderTest.kt b/src/test/kotlin/app/revanced/patcher/ReaderTest.kt deleted file mode 100644 index becd7f6..0000000 --- a/src/test/kotlin/app/revanced/patcher/ReaderTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patcher - -import java.io.ByteArrayOutputStream -import kotlin.test.Test - -internal class ReaderTest { - @Test - fun `read jar containing multiple classes`() { - val testData = PatcherTest::class.java.getResourceAsStream("/test2.jar")!! - Patcher(testData, ByteArrayOutputStream(), PatcherTest.testSignatures).save() // reusing test sigs from PatcherTest - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/util/TestUtil.kt b/src/test/kotlin/app/revanced/patcher/util/TestUtil.kt deleted file mode 100644 index 6d891e1..0000000 --- a/src/test/kotlin/app/revanced/patcher/util/TestUtil.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.revanced.patcher.util - -import org.objectweb.asm.tree.AbstractInsnNode -import org.objectweb.asm.tree.FieldInsnNode -import org.objectweb.asm.tree.LdcInsnNode -import kotlin.test.fail - -object TestUtil { - fun assertNodeEqual(expected: T, actual: T) { - val a = expected.nodeString() - val b = actual.nodeString() - if (a != b) { - fail("expected: $a,\nactual: $b\n") - } - } - - private fun AbstractInsnNode.nodeString(): String { - val sb = NodeStringBuilder() - when (this) { - // TODO(Sculas): Add more types - is LdcInsnNode -> sb - .addType("cst", cst) - is FieldInsnNode -> sb - .addType("owner", owner) - .addType("name", name) - .addType("desc", desc) - } - return "(${this::class.simpleName}): (type = $type, opcode = $opcode, $sb)" - } -} - -private class NodeStringBuilder { - private val sb = StringBuilder() - - fun addType(name: String, value: Any): NodeStringBuilder { - sb.append("$name = \"$value\", ") - return this - } - - override fun toString(): String { - if (sb.isEmpty()) return "" - val s = sb.toString() - return s.substring(0 .. (s.length - 2).coerceAtLeast(0)) // remove the last ", " - } -} diff --git a/src/test/kotlin/patcher/PatcherTest.kt b/src/test/kotlin/patcher/PatcherTest.kt new file mode 100644 index 0000000..f08d023 --- /dev/null +++ b/src/test/kotlin/patcher/PatcherTest.kt @@ -0,0 +1,75 @@ +package app.revanced.patcher + +import app.revanced.patcher.cache.Cache +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultError +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.signature.MethodSignature +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.Opcode +import org.jf.dexlib2.builder.instruction.BuilderInstruction21c +import org.jf.dexlib2.iface.instruction.formats.Instruction21c +import org.jf.dexlib2.iface.reference.FieldReference +import org.jf.dexlib2.immutable.reference.ImmutableMethodReference +import java.io.File + + +fun main() { + val signatures = arrayOf( + MethodSignature( + "main-method", + "V", + AccessFlags.STATIC.value or AccessFlags.PUBLIC.value, + listOf("[O"), + arrayOf( + Opcode.SGET_OBJECT, + Opcode.CONST_STRING, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ) + ) + ) + + val patcher = Patcher( + File("black.apk"), + File("folder/"), + signatures + ) + + val mainMethodPatchViaClassProxy = object : Patch("main-method-patch-via-proxy") { + override fun execute(cache: Cache): PatchResult { + val proxy = cache.findClass { classDef -> + classDef.methods.any { method -> + method.name == "main" + } + } ?: return PatchResultError("Class with method 'mainMethod' could not be found") + + val mainMethodClass = proxy.resolve() + val mainMethod = mainMethodClass.methods.single { method -> method.name == "main" } + + val hideReelMethodRef = ImmutableMethodReference( + "Lfi/razerman/youtube/XAdRemover;", + "HideReel", + listOf("Landroid/view/View;"), + "V" + ) + + val mainMethodInstructions = mainMethod.implementation!!.instructions + val printStreamFieldRef = (mainMethodInstructions.first() as Instruction21c).reference as FieldReference + // TODO: not sure how to use the registers yet, find a way + mainMethodInstructions.add(BuilderInstruction21c(Opcode.SGET_OBJECT, 0, printStreamFieldRef)) + return PatchResultSuccess() + } + } + + val mainMethodPatchViaSignature = object : Patch("main-method-patch-via-signature") { + override fun execute(cache: Cache): PatchResult { + cache.resolvedMethods["main-method"].method + return PatchResultSuccess() + } + } + patcher.addPatches(mainMethodPatchViaClassProxy, mainMethodPatchViaSignature) + patcher.applyPatches() + patcher.save() +} \ No newline at end of file