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 {
|
||||
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"))
|
||||
|
|
|
@ -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<Signature>,
|
||||
) {
|
||||
var cache: Cache
|
||||
input: File,
|
||||
private val output: File,
|
||||
signatures: Array<MethodSignature>,
|
||||
|
||||
private var io: Io
|
||||
private val patches = mutableListOf<Patch>()
|
||||
) {
|
||||
private val cache: Cache
|
||||
private val patches = mutableSetOf<Patch>()
|
||||
|
||||
init {
|
||||
val classes = mutableListOf<ClassNode>()
|
||||
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<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) {
|
||||
|
|
|
@ -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<ClassNode>,
|
||||
val methods: MethodMap
|
||||
)
|
||||
internal val classes: Set<ClassDef>,
|
||||
val resolvedMethods: MethodMap
|
||||
) {
|
||||
internal val classProxy = mutableListOf<ClassProxy>()
|
||||
|
||||
class MethodMap : LinkedHashMap<String, PatchData>() {
|
||||
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<String, MethodSignatureScanResult>() {
|
||||
override fun get(key: String): MethodSignatureScanResult {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ClassNode>, private val signatures: Array<Signature>) {
|
||||
// TODO: add logger
|
||||
internal class MethodResolver(private val classes: Set<ClassDef>, private val signatures: Array<MethodSignature>) {
|
||||
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<ClassNode>, 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<ClassNode>, 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<ClassNode>, private va
|
|||
return null
|
||||
}
|
||||
|
||||
private fun cmp(method: MethodNode, signature: Signature): Pair<Boolean, ScanResult?> {
|
||||
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<Boolean, MethodResolverScanResult?> {
|
||||
// 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 <T> MutableIterable<T>.scanFor(pattern: Array<Opcode>): 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> T.shouldSkip(): Boolean {
|
||||
return this == Opcode.GOTO // TODO: and: this == AbstractInsnNode.LINE
|
||||
}
|
||||
|
||||
private fun Array<Type>.convertObjects(): Array<Type> {
|
||||
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<Type>.convertObjects(): Array<Type> {
|
||||
// return this.map { it.convertObject() }.toTypedArray()
|
||||
// }
|
||||
|
||||
private fun AbstractInsnNode.shouldSkip() =
|
||||
type == AbstractInsnNode.LABEL || type == AbstractInsnNode.LINE
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package app.revanced.patcher.resolver
|
||||
|
||||
internal data class ScanResult(
|
||||
internal data class MethodResolverScanResult(
|
||||
val found: Boolean,
|
||||
val startIndex: 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