mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 01:02:22 +01:00
feat: migrate to dexlib
BREAKING CHANGE: Removed usage of ASM library
This commit is contained in:
parent
fa0412985c
commit
be51f42710
25 changed files with 590 additions and 541 deletions
|
@ -12,10 +12,7 @@ repositories {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib"))
|
implementation(kotlin("stdlib"))
|
||||||
implementation("org.ow2.asm:asm:9.2")
|
implementation("com.github.lanchon.dexpatcher:multidexlib2:2.3.4")
|
||||||
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("io.github.microutils:kotlin-logging:2.1.21")
|
implementation("io.github.microutils:kotlin-logging:2.1.21")
|
||||||
testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger!
|
testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger!
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|
|
@ -3,49 +3,51 @@ package app.revanced.patcher
|
||||||
import app.revanced.patcher.cache.Cache
|
import app.revanced.patcher.cache.Cache
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.resolver.MethodResolver
|
import app.revanced.patcher.resolver.MethodResolver
|
||||||
import app.revanced.patcher.signature.Signature
|
import app.revanced.patcher.signature.MethodSignature
|
||||||
import app.revanced.patcher.util.Io
|
import lanchon.multidexlib2.BasicDexFileNamer
|
||||||
import org.objectweb.asm.tree.ClassNode
|
import lanchon.multidexlib2.MultiDexIO
|
||||||
import java.io.IOException
|
import org.jf.dexlib2.Opcodes
|
||||||
import java.io.InputStream
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
import java.io.OutputStream
|
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(
|
class Patcher(
|
||||||
private val input: InputStream,
|
input: File,
|
||||||
private val output: OutputStream,
|
private val output: File,
|
||||||
signatures: Array<Signature>,
|
signatures: Array<MethodSignature>,
|
||||||
) {
|
|
||||||
var cache: Cache
|
|
||||||
|
|
||||||
private var io: Io
|
) {
|
||||||
private val patches = mutableListOf<Patch>()
|
private val cache: Cache
|
||||||
|
private val patches = mutableSetOf<Patch>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val classes = mutableListOf<ClassNode>()
|
// TODO: find a way to load all dex classes, the code below only loads the first .dex file
|
||||||
io = Io(input, output, classes)
|
val dexFile = MultiDexIO.readDexFile(true, input, BasicDexFileNamer(), Opcodes.getDefault(), null)
|
||||||
io.readFromJar()
|
cache = Cache(dexFile.classes, MethodResolver(dexFile.classes, signatures).resolve())
|
||||||
cache = Cache(classes, MethodResolver(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() {
|
fun save() {
|
||||||
io.saveAsJar()
|
val newDexFile = object : DexFile {
|
||||||
|
override fun getClasses(): MutableSet<out ClassDef> {
|
||||||
|
// 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) {
|
fun addPatches(vararg patches: Patch) {
|
||||||
|
@ -67,4 +69,4 @@ class Patcher(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,30 @@
|
||||||
package app.revanced.patcher.cache
|
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(
|
class Cache(
|
||||||
val classes: List<ClassNode>,
|
internal val classes: Set<ClassDef>,
|
||||||
val methods: MethodMap
|
val resolvedMethods: MethodMap
|
||||||
)
|
) {
|
||||||
|
internal val classProxy = mutableListOf<ClassProxy>()
|
||||||
|
|
||||||
class MethodMap : LinkedHashMap<String, PatchData>() {
|
fun findClass(predicate: (ClassDef) -> Boolean): ClassProxy? {
|
||||||
override fun get(key: String): PatchData {
|
// 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<String, MethodSignatureScanResult>() {
|
||||||
|
override fun get(key: String): MethodSignatureScanResult {
|
||||||
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
|
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
21
src/main/kotlin/app/revanced/patcher/cache/proxy/ClassProxy.kt
vendored
Normal file
21
src/main/kotlin/app/revanced/patcher/cache/proxy/ClassProxy.kt
vendored
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
29
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotation.kt
vendored
Normal file
29
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotation.kt
vendored
Normal file
|
@ -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<MutableAnnotationElement> {
|
||||||
|
return elements
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVisibility(): Int {
|
||||||
|
return visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Annotation.toMutable(): MutableAnnotation {
|
||||||
|
return MutableAnnotation(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotationElement.kt
vendored
Normal file
33
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableAnnotationElement.kt
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
94
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableClass.kt
vendored
Normal file
94
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableClass.kt
vendored
Normal file
|
@ -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<String> {
|
||||||
|
return interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAnnotations(): MutableSet<MutableAnnotation> {
|
||||||
|
return annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStaticFields(): MutableSet<MutableField> {
|
||||||
|
return staticFields
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInstanceFields(): MutableSet<MutableField> {
|
||||||
|
return instanceFields
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFields(): MutableSet<MutableField> {
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDirectMethods(): MutableSet<MutableMethod> {
|
||||||
|
return directMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getVirtualMethods(): MutableSet<MutableMethod> {
|
||||||
|
return virtualMethods
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMethods(): MutableSet<MutableMethod> {
|
||||||
|
return methods
|
||||||
|
}
|
||||||
|
}
|
26
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableEncodedValue.kt
vendored
Normal file
26
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableEncodedValue.kt
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableField.kt
vendored
Normal file
65
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableField.kt
vendored
Normal file
|
@ -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<MutableAnnotation> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethod.kt
vendored
Normal file
58
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethod.kt
vendored
Normal file
|
@ -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<CharSequence> {
|
||||||
|
return parameterTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReturnType(): String {
|
||||||
|
return returnType
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAnnotations(): MutableSet<MutableAnnotation> {
|
||||||
|
return annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAccessFlags(): Int {
|
||||||
|
return accessFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getParameters(): MutableList<MutableMethodParameter> {
|
||||||
|
return parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getImplementation(): MutableMethodImplementation? {
|
||||||
|
return implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun Method.toMutable(): MutableMethod {
|
||||||
|
return MutableMethod(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethodParameter.kt
vendored
Normal file
35
src/main/kotlin/app/revanced/patcher/cache/proxy/mutableTypes/MutableMethodParameter.kt
vendored
Normal file
|
@ -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<MutableAnnotation> {
|
||||||
|
return annotations
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun MethodParameter.toMutable(): MutableMethodParameter {
|
||||||
|
return MutableMethodParameter(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,4 +4,4 @@ import app.revanced.patcher.cache.Cache
|
||||||
|
|
||||||
abstract class Patch(val patchName: String) {
|
abstract class Patch(val patchName: String) {
|
||||||
abstract fun execute(cache: Cache): PatchResult
|
abstract fun execute(cache: Cache): PatchResult
|
||||||
}
|
}
|
|
@ -1,36 +1,31 @@
|
||||||
package app.revanced.patcher.resolver
|
package app.revanced.patcher.resolver
|
||||||
|
|
||||||
import app.revanced.patcher.cache.MethodMap
|
import app.revanced.patcher.cache.MethodMap
|
||||||
import app.revanced.patcher.cache.PatchData
|
import app.revanced.patcher.signature.MethodSignatureScanResult
|
||||||
import app.revanced.patcher.cache.PatternScanData
|
import app.revanced.patcher.signature.PatternScanData
|
||||||
import app.revanced.patcher.signature.Signature
|
import app.revanced.patcher.signature.MethodSignature
|
||||||
import app.revanced.patcher.util.ExtraTypes
|
import org.jf.dexlib2.Opcode
|
||||||
import mu.KotlinLogging
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
import org.objectweb.asm.Type
|
import org.jf.dexlib2.iface.Method
|
||||||
import org.objectweb.asm.tree.*
|
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger("MethodResolver")
|
// TODO: add logger
|
||||||
|
internal class MethodResolver(private val classes: Set<ClassDef>, private val signatures: Array<MethodSignature>) {
|
||||||
internal class MethodResolver(private val classList: List<ClassNode>, private val signatures: Array<Signature>) {
|
|
||||||
fun resolve(): MethodMap {
|
fun resolve(): MethodMap {
|
||||||
val methodMap = MethodMap()
|
val methodMap = MethodMap()
|
||||||
|
|
||||||
for ((classNode, methods) in classList) {
|
for (classDef in classes) {
|
||||||
for (method in methods) {
|
for (method in classDef.methods) {
|
||||||
for (signature in signatures) {
|
for (methodSignature in signatures) {
|
||||||
if (methodMap.containsKey(signature.name)) { // method already found for this sig
|
if (methodMap.containsKey(methodSignature.name)) { // method already found for this sig
|
||||||
logger.trace { "Sig ${signature.name} already found, skipping." }
|
|
||||||
continue
|
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) {
|
if (!r || sr == null) {
|
||||||
logger.trace { "Compare result for sig ${signature.name} has failed!" }
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.trace { "Method for sig ${signature.name} found!" }
|
|
||||||
methodMap[signature.name] = PatchData(
|
methodMap[methodSignature.name] = MethodSignatureScanResult(
|
||||||
classNode,
|
|
||||||
method,
|
method,
|
||||||
PatternScanData(
|
PatternScanData(
|
||||||
// sadly we cannot create contracts for a data class, so we must assert
|
// sadly we cannot create contracts for a data class, so we must assert
|
||||||
|
@ -44,7 +39,6 @@ internal class MethodResolver(private val classList: List<ClassNode>, private va
|
||||||
|
|
||||||
for (signature in signatures) {
|
for (signature in signatures) {
|
||||||
if (methodMap.containsKey(signature.name)) continue
|
if (methodMap.containsKey(signature.name)) continue
|
||||||
logger.error { "Could not find method for sig ${signature.name}!" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return methodMap
|
return methodMap
|
||||||
|
@ -52,12 +46,11 @@ internal class MethodResolver(private val classList: List<ClassNode>, private va
|
||||||
|
|
||||||
// These functions do not require the constructor values, so they can be static.
|
// These functions do not require the constructor values, so they can be static.
|
||||||
companion object {
|
companion object {
|
||||||
fun resolveMethod(classNode: ClassNode, signature: Signature): PatchData? {
|
fun resolveMethod(classNode: ClassDef, signature: MethodSignature): MethodSignatureScanResult? {
|
||||||
for (method in classNode.methods) {
|
for (method in classNode.methods) {
|
||||||
val (r, sr) = cmp(method, signature)
|
val (r, sr) = cmp(method, signature)
|
||||||
if (!r || sr == null) continue
|
if (!r || sr == null) continue
|
||||||
return PatchData(
|
return MethodSignatureScanResult(
|
||||||
classNode,
|
|
||||||
method,
|
method,
|
||||||
PatternScanData(0, 0) // opcode list is always ignored.
|
PatternScanData(0, 0) // opcode list is always ignored.
|
||||||
)
|
)
|
||||||
|
@ -65,92 +58,72 @@ internal class MethodResolver(private val classList: List<ClassNode>, private va
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cmp(method: MethodNode, signature: Signature): Pair<Boolean, ScanResult?> {
|
private fun cmp(method: Method, signature: MethodSignature): Pair<Boolean, MethodResolverScanResult?> {
|
||||||
signature.returns?.let { _ ->
|
// TODO: compare as generic object if not primitive
|
||||||
val methodReturns = Type.getReturnType(method.desc).convertObject()
|
signature.returnType?.let { _ ->
|
||||||
if (signature.returns != methodReturns) {
|
if (signature.returnType != method.returnType) {
|
||||||
logger.trace {
|
|
||||||
"""
|
|
||||||
Comparing sig ${signature.name}: invalid return type:
|
|
||||||
expected ${signature.returns},
|
|
||||||
got $methodReturns
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
return@cmp false to null
|
return@cmp false to null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signature.accessors?.let { _ ->
|
signature.accessFlags?.let { _ ->
|
||||||
if (signature.accessors != method.access) {
|
if (signature.accessFlags != method.accessFlags) {
|
||||||
logger.trace {
|
|
||||||
"""
|
|
||||||
Comparing sig ${signature.name}: invalid accessors:
|
|
||||||
expected ${signature.accessors},
|
|
||||||
got ${method.access}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
return@cmp false to null
|
return@cmp false to null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signature.parameters?.let { _ ->
|
// TODO: compare as generic object if the parameter is not primitive
|
||||||
val parameters = Type.getArgumentTypes(method.desc).convertObjects()
|
signature.methodParameters?.let { _ ->
|
||||||
if (!signature.parameters.contentEquals(parameters)) {
|
if (signature.methodParameters != method.parameters) {
|
||||||
logger.trace {
|
|
||||||
"""
|
|
||||||
Comparing sig ${signature.name}: invalid parameter types:
|
|
||||||
expected ${signature.parameters.joinToString()}},
|
|
||||||
got ${parameters.joinToString()}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
|
||||||
return@cmp false to null
|
return@cmp false to null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signature.opcodes?.let { _ ->
|
signature.opcodes?.let { _ ->
|
||||||
val result = method.instructions.scanFor(signature.opcodes)
|
val result = method.implementation?.instructions?.scanFor(signature.opcodes)
|
||||||
if (!result.found) {
|
return@cmp if (result != null && result.found) true to result else false to null
|
||||||
logger.trace { "Comparing sig ${signature.name}: invalid opcode pattern" }
|
|
||||||
return@cmp false to null
|
|
||||||
}
|
|
||||||
return@cmp true to result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true to ScanResult(true)
|
return true to MethodResolverScanResult(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private operator fun ClassNode.component1() = this
|
private operator fun ClassDef.component1() = this
|
||||||
private operator fun ClassNode.component2() = this.methods
|
private operator fun ClassDef.component2() = this.methods
|
||||||
|
|
||||||
private fun InsnList.scanFor(pattern: IntArray): ScanResult {
|
private fun <T> MutableIterable<T>.scanFor(pattern: Array<Opcode>): MethodResolverScanResult {
|
||||||
for (i in 0 until this.size()) {
|
// TODO: create var for count?
|
||||||
|
for (i in 0 until this.count()) {
|
||||||
var occurrence = 0
|
var occurrence = 0
|
||||||
while (i + occurrence < this.size()) {
|
while (i + occurrence < this.count()) {
|
||||||
val n = this[i + occurrence]
|
val n = this.elementAt(i + occurrence)
|
||||||
if (!n.shouldSkip() && n.opcode != pattern[occurrence]) break
|
if (!n.shouldSkip() && n != pattern[occurrence]) break
|
||||||
if (++occurrence >= pattern.size) {
|
if (++occurrence >= pattern.size) {
|
||||||
val current = i + occurrence
|
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 {
|
// TODO: extend Opcode type, not T (requires a cast to Opcode)
|
||||||
return when (this.sort) {
|
private fun <T> T.shouldSkip(): Boolean {
|
||||||
Type.OBJECT -> ExtraTypes.Any
|
return this == Opcode.GOTO // TODO: and: this == AbstractInsnNode.LINE
|
||||||
Type.ARRAY -> ExtraTypes.ArrayAny
|
|
||||||
else -> this
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Array<Type>.convertObjects(): Array<Type> {
|
// TODO: use this somehow to compare types as generic objects if not primitive
|
||||||
return this.map { it.convertObject() }.toTypedArray()
|
// private fun Type.convertObject(): Type {
|
||||||
}
|
// return when (this.sort) {
|
||||||
|
// Type.OBJECT -> ExtraTypes.Any
|
||||||
|
// Type.ARRAY -> ExtraTypes.ArrayAny
|
||||||
|
// else -> this
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private fun Array<Type>.convertObjects(): Array<Type> {
|
||||||
|
// return this.map { it.convertObject() }.toTypedArray()
|
||||||
|
// }
|
||||||
|
|
||||||
private fun AbstractInsnNode.shouldSkip() =
|
|
||||||
type == AbstractInsnNode.LABEL || type == AbstractInsnNode.LINE
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package app.revanced.patcher.resolver
|
package app.revanced.patcher.resolver
|
||||||
|
|
||||||
internal data class ScanResult(
|
internal data class MethodResolverScanResult(
|
||||||
val found: Boolean,
|
val found: Boolean,
|
||||||
val startIndex: Int? = 0,
|
val startIndex: Int? = 0,
|
||||||
val endIndex: Int? = 0
|
val endIndex: Int? = 0
|
||||||
)
|
)
|
|
@ -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<CharSequence>?,
|
||||||
|
val opcodes: Array<Opcode>?
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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<Type>?,
|
|
||||||
val opcodes: IntArray?
|
|
||||||
)
|
|
|
@ -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<Any>::class.java)
|
|
||||||
}
|
|
|
@ -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<ClassNode>
|
|
||||||
) {
|
|
||||||
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<String, ClassReader>()
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<out AbstractInsnNode>.toInsnList(): InsnList {
|
|
||||||
val list = InsnList()
|
|
||||||
this.forEach { list.add(it) }
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<Signature> = 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
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <T: AbstractInsnNode> 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 ", "
|
|
||||||
}
|
|
||||||
}
|
|
75
src/test/kotlin/patcher/PatcherTest.kt
Normal file
75
src/test/kotlin/patcher/PatcherTest.kt
Normal file
|
@ -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()
|
||||||
|
}
|
Loading…
Reference in a new issue