mirror of
https://github.com/revanced/revanced-patcher.git
synced 2025-05-02 05:54:26 +02:00
refact: include each signature in its corresponding patch
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
b2dab3fabf
commit
1f08da8b2a
@ -1,11 +1,9 @@
|
|||||||
package app.revanced.patcher
|
package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.cache.Cache
|
|
||||||
import app.revanced.patcher.cache.findIndexed
|
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.PatchMetadata
|
import app.revanced.patcher.patch.PatchMetadata
|
||||||
import app.revanced.patcher.patch.PatchResultSuccess
|
import app.revanced.patcher.patch.PatchResultSuccess
|
||||||
import app.revanced.patcher.signature.MethodSignature
|
import app.revanced.patcher.proxy.ClassProxy
|
||||||
import app.revanced.patcher.signature.resolver.SignatureResolver
|
import app.revanced.patcher.signature.resolver.SignatureResolver
|
||||||
import app.revanced.patcher.util.ListBackedSet
|
import app.revanced.patcher.util.ListBackedSet
|
||||||
import lanchon.multidexlib2.BasicDexFileNamer
|
import lanchon.multidexlib2.BasicDexFileNamer
|
||||||
@ -22,21 +20,18 @@ val NAMER = BasicDexFileNamer()
|
|||||||
/**
|
/**
|
||||||
* ReVanced Patcher.
|
* ReVanced Patcher.
|
||||||
* @param input The input file (an apk or any other multi dex container).
|
* @param input The input file (an apk or any other multi dex container).
|
||||||
* @param signatures A list of method signatures for the patches.
|
|
||||||
*/
|
*/
|
||||||
class Patcher(
|
class Patcher(
|
||||||
input: File,
|
input: File,
|
||||||
private val signatures: Iterable<MethodSignature>,
|
|
||||||
) {
|
) {
|
||||||
private val cache: Cache
|
private val patcherData: PatcherData
|
||||||
private val patches = mutableSetOf<Patch>()
|
|
||||||
private val opcodes: Opcodes
|
private val opcodes: Opcodes
|
||||||
private var sigsResolved = false
|
private var signaturesResolved = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val dexFile = MultiDexIO.readDexFile(true, input, NAMER, null, null)
|
val dexFile = MultiDexIO.readDexFile(true, input, NAMER, null, null)
|
||||||
opcodes = dexFile.opcodes
|
opcodes = dexFile.opcodes
|
||||||
cache = Cache(dexFile.classes.toMutableList())
|
patcherData = PatcherData(dexFile.classes.toMutableList())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,18 +48,18 @@ class Patcher(
|
|||||||
for (file in files) {
|
for (file in files) {
|
||||||
val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null)
|
val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null)
|
||||||
for (classDef in dexFile.classes) {
|
for (classDef in dexFile.classes) {
|
||||||
val e = cache.classes.findIndexed { it.type == classDef.type }
|
val e = patcherData.classes.findIndexed { it.type == classDef.type }
|
||||||
if (e != null) {
|
if (e != null) {
|
||||||
if (throwOnDuplicates) {
|
if (throwOnDuplicates) {
|
||||||
throw Exception("Class ${classDef.type} has already been added to the patcher.")
|
throw Exception("Class ${classDef.type} has already been added to the patcher.")
|
||||||
}
|
}
|
||||||
val (_, idx) = e
|
val (_, idx) = e
|
||||||
if (allowedOverwrites.contains(classDef.type)) {
|
if (allowedOverwrites.contains(classDef.type)) {
|
||||||
cache.classes[idx] = classDef
|
patcherData.classes[idx] = classDef
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cache.classes.add(classDef)
|
patcherData.classes.add(classDef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,16 +69,27 @@ class Patcher(
|
|||||||
*/
|
*/
|
||||||
fun save(): Map<String, MemoryDataStore> {
|
fun save(): Map<String, MemoryDataStore> {
|
||||||
val newDexFile = object : DexFile {
|
val newDexFile = object : DexFile {
|
||||||
|
private fun MutableList<ClassDef>.replaceWith(proxy: ClassProxy) {
|
||||||
|
if (proxy.proxyUsed) return
|
||||||
|
this[proxy.originalIndex] = proxy.mutatedClass
|
||||||
|
}
|
||||||
|
|
||||||
override fun getClasses(): Set<ClassDef> {
|
override fun getClasses(): Set<ClassDef> {
|
||||||
cache.methodMap.values.forEach {
|
for (proxy in patcherData.classProxies) {
|
||||||
if (it.definingClassProxy.proxyUsed) {
|
patcherData.classes.replaceWith(proxy)
|
||||||
cache.classes[it.definingClassProxy.originalIndex] = it.definingClassProxy.mutatedClass
|
}
|
||||||
|
for (patch in patcherData.patches) {
|
||||||
|
for (signature in patch.signatures) {
|
||||||
|
val result = signature.result
|
||||||
|
result ?: continue
|
||||||
|
|
||||||
|
val proxy = result.definingClassProxy
|
||||||
|
if (!proxy.proxyUsed) continue
|
||||||
|
|
||||||
|
patcherData.classes.replaceWith(proxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache.classProxy.filter { it.proxyUsed }.forEach { proxy ->
|
return ListBackedSet(patcherData.classes)
|
||||||
cache.classes[proxy.originalIndex] = proxy.mutatedClass
|
|
||||||
}
|
|
||||||
return ListBackedSet(cache.classes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getOpcodes(): Opcodes {
|
override fun getOpcodes(): Opcodes {
|
||||||
@ -106,29 +112,31 @@ class Patcher(
|
|||||||
* @param patches The patches to add.
|
* @param patches The patches to add.
|
||||||
*/
|
*/
|
||||||
fun addPatches(patches: Iterable<Patch>) {
|
fun addPatches(patches: Iterable<Patch>) {
|
||||||
this.patches.addAll(patches)
|
patcherData.patches.addAll(patches)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply patches loaded into the patcher.
|
* Apply patches loaded into the patcher.
|
||||||
* @param stopOnError If true, the patches will stop on the first error.
|
* @param stopOnError If true, the patches will stop on the first error.
|
||||||
* @return A map of results. If the patch was successfully applied,
|
* @return A map of [PatchResultSuccess]. If the [Patch] was successfully applied,
|
||||||
* PatchResultSuccess will always be returned in the wrapping Result object.
|
* [PatchResultSuccess] will always be returned to the wrapping Result object.
|
||||||
* If the patch failed to apply, an Exception will always be returned in the wrapping Result object.
|
* If the [Patch] failed to apply, an Exception will always be returned to the wrapping Result object.
|
||||||
*/
|
*/
|
||||||
fun applyPatches(
|
fun applyPatches(
|
||||||
stopOnError: Boolean = false,
|
stopOnError: Boolean = false,
|
||||||
callback: (String) -> Unit = {}
|
callback: (String) -> Unit = {}
|
||||||
): Map<PatchMetadata, Result<PatchResultSuccess>> {
|
): Map<PatchMetadata, Result<PatchResultSuccess>> {
|
||||||
if (!sigsResolved) {
|
|
||||||
SignatureResolver(cache.classes, signatures).resolve(cache.methodMap)
|
if (!signaturesResolved) {
|
||||||
sigsResolved = true
|
val signatures = patcherData.patches.flatMap { it.signatures }
|
||||||
|
SignatureResolver(patcherData.classes, signatures).resolve()
|
||||||
|
signaturesResolved = true
|
||||||
}
|
}
|
||||||
return buildMap {
|
return buildMap {
|
||||||
for (patch in patches) {
|
for (patch in patcherData.patches) {
|
||||||
callback(patch.metadata.shortName)
|
callback(patch.metadata.shortName)
|
||||||
val result: Result<PatchResultSuccess> = try {
|
val result: Result<PatchResultSuccess> = try {
|
||||||
val pr = patch.execute(cache)
|
val pr = patch.execute(patcherData)
|
||||||
if (pr.isSuccess()) {
|
if (pr.isSuccess()) {
|
||||||
Result.success(pr.success()!!)
|
Result.success(pr.success()!!)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
package app.revanced.patcher.cache
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.proxy.ClassProxy
|
import app.revanced.patcher.proxy.ClassProxy
|
||||||
import app.revanced.patcher.signature.SignatureResolverResult
|
import app.revanced.patcher.signature.SignatureResolverResult
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
|
||||||
class Cache(
|
class PatcherData(
|
||||||
internal val classes: MutableList<ClassDef>,
|
internal val classes: MutableList<ClassDef>,
|
||||||
val methodMap: MethodMap = MethodMap()
|
|
||||||
) {
|
) {
|
||||||
// TODO: currently we create ClassProxies at multiple places, which is why we could have merge conflicts
|
internal val classProxies = mutableSetOf<ClassProxy>()
|
||||||
// this can be solved by creating a dedicated method for creating class proxies,
|
internal val patches = mutableSetOf<Patch>()
|
||||||
// if the class proxy already exists in the cached proxy list below.
|
|
||||||
// The to-do in the method findClass is related
|
|
||||||
internal val classProxy = mutableSetOf<ClassProxy>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a class by a given class name
|
* Find a class by a given class name
|
||||||
@ -25,23 +22,23 @@ class Cache(
|
|||||||
* @return A proxy for the first class that matches the predicate
|
* @return A proxy for the first class that matches the predicate
|
||||||
*/
|
*/
|
||||||
fun findClass(predicate: (ClassDef) -> Boolean): ClassProxy? {
|
fun findClass(predicate: (ClassDef) -> Boolean): ClassProxy? {
|
||||||
// TODO: find a cleaner way to store all proxied classes.
|
// if we already proxied the class matching the predicate...
|
||||||
// Currently we have to search the method map as well as the class proxy list which is not elegant
|
for (patch in patches) {
|
||||||
|
for (signature in patch.signatures) {
|
||||||
|
val result = signature.result
|
||||||
|
result ?: continue
|
||||||
|
|
||||||
// if we already proxied the class matching the predicate,
|
if (predicate(result.definingClassProxy.immutableClass))
|
||||||
val proxiedClass = classProxy.find { predicate(it.immutableClass) }
|
return result.definingClassProxy // ...then return that proxy
|
||||||
// return that proxy
|
}
|
||||||
if (proxiedClass != null) return proxiedClass
|
}
|
||||||
// if we already have the class matching the predicate in the method map,
|
|
||||||
val result = methodMap.entries.find { predicate(it.value.definingClassProxy.immutableClass) }?.value
|
|
||||||
if (result != null) return result.definingClassProxy
|
|
||||||
|
|
||||||
// else search the original class list
|
// else search the original class list
|
||||||
val (foundClass, index) = classes.findIndexed(predicate) ?: return null
|
val (foundClass, index) = classes.findIndexed(predicate) ?: return null
|
||||||
// create a class proxy with the index of the class in the classes list
|
// create a class proxy with the index of the class in the classes list
|
||||||
val classProxy = ClassProxy(foundClass, index)
|
val classProxy = ClassProxy(foundClass, index)
|
||||||
// add it to the cache and
|
// add it to the cache and
|
||||||
this.classProxy.add(classProxy)
|
this.classProxies.add(classProxy)
|
||||||
// return the proxy class
|
// return the proxy class
|
||||||
return classProxy
|
return classProxy
|
||||||
}
|
}
|
||||||
@ -55,7 +52,7 @@ class MethodMap : LinkedHashMap<String, SignatureResolverResult>() {
|
|||||||
|
|
||||||
internal class MethodNotFoundException(s: String) : Exception(s)
|
internal class MethodNotFoundException(s: String) : Exception(s)
|
||||||
|
|
||||||
internal inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
|
internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
|
||||||
for (element in this) {
|
for (element in this) {
|
||||||
if (predicate(element)) {
|
if (predicate(element)) {
|
||||||
return element
|
return element
|
@ -15,4 +15,4 @@ fun MutableMethodImplementation.addInstructions(index: Int, instructions: List<B
|
|||||||
for (i in instructions.lastIndex downTo 0) {
|
for (i in instructions.lastIndex downTo 0) {
|
||||||
this.addInstruction(index, instructions[i])
|
this.addInstruction(index, instructions[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,36 @@
|
|||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.cache.Cache
|
import app.revanced.patcher.PatcherData
|
||||||
|
import app.revanced.patcher.signature.MethodSignature
|
||||||
|
|
||||||
abstract class Patch(val metadata: PatchMetadata) {
|
/**
|
||||||
abstract fun execute(cache: Cache): PatchResult
|
* Patch for the Patcher.
|
||||||
|
* @param metadata [PatchMetadata] for the patch.
|
||||||
|
* @param signatures A list of [MethodSignature] this patch relies on.
|
||||||
|
*/
|
||||||
|
abstract class Patch(
|
||||||
|
val metadata: PatchMetadata,
|
||||||
|
val signatures: Iterable<MethodSignature>
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main function of the [Patch] which the patcher will call.
|
||||||
|
*/
|
||||||
|
abstract fun execute(patcherData: PatcherData): PatchResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a [Patch].
|
||||||
|
* @param shortName A suggestive short name for the [Patch].
|
||||||
|
* @param name A suggestive name for the [Patch].
|
||||||
|
* @param description A description for the [Patch].
|
||||||
|
* @param compatiblePackages A list of packages this [Patch] is compatible with.
|
||||||
|
* @param version The version of the [Patch].
|
||||||
|
*/
|
||||||
data class PatchMetadata(
|
data class PatchMetadata(
|
||||||
val shortName: String,
|
val shortName: String,
|
||||||
val fullName: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
|
@Suppress("ArrayInDataClass") val compatiblePackages: Array<String>,
|
||||||
|
val version: String,
|
||||||
)
|
)
|
@ -3,65 +3,66 @@ package app.revanced.patcher.signature
|
|||||||
import org.jf.dexlib2.Opcode
|
import org.jf.dexlib2.Opcode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a method signature.
|
* Represents the [MethodSignature] for a method.
|
||||||
* @param name A suggestive name for the method which the signature was created for.
|
* @param methodSignatureMetadata Metadata for this [MethodSignature].
|
||||||
* @param metadata Metadata about this signature.
|
|
||||||
* @param returnType The return type of the method.
|
* @param returnType The return type of the method.
|
||||||
* @param accessFlags The access flags of the method.
|
* @param accessFlags The access flags of the method.
|
||||||
* @param methodParameters The parameters of the method.
|
* @param methodParameters The parameters of the method.
|
||||||
* @param opcodes A list of opcodes of the method.
|
* @param opcodes The list of opcodes of the method.
|
||||||
*/
|
*/
|
||||||
data class MethodSignature(
|
class MethodSignature(
|
||||||
|
val methodSignatureMetadata: MethodSignatureMetadata,
|
||||||
|
internal val returnType: String?,
|
||||||
|
internal val accessFlags: Int?,
|
||||||
|
internal val methodParameters: Iterable<String>?,
|
||||||
|
internal val opcodes: Iterable<Opcode>?
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* The result of the signature
|
||||||
|
*/
|
||||||
|
var result: SignatureResolverResult? = null // TODO: figure out how to get rid of nullable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about a [MethodSignature].
|
||||||
|
* @param name A suggestive name for the [MethodSignature].
|
||||||
|
* @param methodMetadata Metadata about the method for the [MethodSignature].
|
||||||
|
* @param patternScanMethod The pattern scanning method the pattern scanner should rely on.
|
||||||
|
* Can either be [PatternScanMethod.Fuzzy] or [PatternScanMethod.Direct].
|
||||||
|
* @param description An optional description of the [MethodSignature].
|
||||||
|
* @param compatiblePackages The list of packages the [MethodSignature] is compatible with.
|
||||||
|
* @param version The version of this signature.
|
||||||
|
*/
|
||||||
|
data class MethodSignatureMetadata(
|
||||||
val name: String,
|
val name: String,
|
||||||
val metadata: SignatureMetadata,
|
val methodMetadata: MethodMetadata,
|
||||||
val returnType: String?,
|
val patternScanMethod: PatternScanMethod,
|
||||||
val accessFlags: Int?,
|
@Suppress("ArrayInDataClass") val compatiblePackages: Array<String>,
|
||||||
val methodParameters: Iterable<String>?,
|
val description: String?,
|
||||||
val opcodes: Iterable<Opcode>?
|
val version: String
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata about the signature.
|
* Metadata about the method for a [MethodSignature].
|
||||||
* @param method Metadata about the method for this signature.
|
* @param definingClass The defining class name of the method.
|
||||||
* @param patcher Metadata for the Patcher, this contains things like how the Patcher should interpret this signature.
|
* @param name A suggestive name for the method which the [MethodSignature] was created for.
|
||||||
*/
|
|
||||||
data class SignatureMetadata(
|
|
||||||
val method: MethodMetadata,
|
|
||||||
val patcher: PatcherMetadata
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Metadata about the method for this signature.
|
|
||||||
* @param definingClass The defining class name of the original method.
|
|
||||||
* @param methodName The name of the original method.
|
|
||||||
* @param comment A comment about this method and the data above.
|
|
||||||
* For example, the version this signature was originally made for.
|
|
||||||
*/
|
*/
|
||||||
data class MethodMetadata(
|
data class MethodMetadata(
|
||||||
val definingClass: String?,
|
val definingClass: String?,
|
||||||
val methodName: String?,
|
val name: String?
|
||||||
val comment: String
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata for the Patcher, this contains things like how the Patcher should interpret this signature.
|
* The method, the patcher should rely on when scanning the opcode pattern of a [MethodSignature]
|
||||||
* @param resolverMethod The method the resolver should use to resolve the signature.
|
|
||||||
*/
|
*/
|
||||||
data class PatcherMetadata(
|
interface PatternScanMethod {
|
||||||
val resolverMethod: ResolverMethod
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The method the resolver should use to resolve the signature.
|
|
||||||
*/
|
|
||||||
interface ResolverMethod {
|
|
||||||
/**
|
/**
|
||||||
* When comparing the signature, if one or more of the opcodes do not match, skip.
|
* When comparing the signature, if one or more of the opcodes do not match, skip.
|
||||||
*/
|
*/
|
||||||
class Direct : ResolverMethod
|
class Direct : PatternScanMethod
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When comparing the signature, if [threshold] or more of the opcodes do not match, skip.
|
* When comparing the signature, if [threshold] or more of the opcodes do not match, skip.
|
||||||
*/
|
*/
|
||||||
class Fuzzy(val threshold: Int) : ResolverMethod
|
class Fuzzy(internal val threshold: Int) : PatternScanMethod
|
||||||
}
|
}
|
@ -1,9 +1,8 @@
|
|||||||
package app.revanced.patcher.signature.resolver
|
package app.revanced.patcher.signature.resolver
|
||||||
|
|
||||||
import app.revanced.patcher.cache.MethodMap
|
|
||||||
import app.revanced.patcher.proxy.ClassProxy
|
import app.revanced.patcher.proxy.ClassProxy
|
||||||
import app.revanced.patcher.signature.MethodSignature
|
import app.revanced.patcher.signature.MethodSignature
|
||||||
import app.revanced.patcher.signature.ResolverMethod
|
import app.revanced.patcher.signature.PatternScanMethod
|
||||||
import app.revanced.patcher.signature.PatternScanResult
|
import app.revanced.patcher.signature.PatternScanResult
|
||||||
import app.revanced.patcher.signature.SignatureResolverResult
|
import app.revanced.patcher.signature.SignatureResolverResult
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
@ -14,19 +13,17 @@ internal class SignatureResolver(
|
|||||||
private val classes: List<ClassDef>,
|
private val classes: List<ClassDef>,
|
||||||
private val methodSignatures: Iterable<MethodSignature>
|
private val methodSignatures: Iterable<MethodSignature>
|
||||||
) {
|
) {
|
||||||
fun resolve(methodMap: MethodMap) {
|
fun resolve() {
|
||||||
for ((index, classDef) in classes.withIndex()) {
|
for ((index, classDef) in classes.withIndex()) {
|
||||||
for (signature in methodSignatures) {
|
for (signature in methodSignatures) {
|
||||||
if (methodMap.containsKey(signature.name)) {
|
if (signature.result != null) continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (method in classDef.methods) {
|
for (method in classDef.methods) {
|
||||||
val patternScanData = compareSignatureToMethod(signature, method) ?: continue
|
val patternScanData = compareSignatureToMethod(signature, method) ?: continue
|
||||||
|
|
||||||
// create class proxy, in case a patch needs mutability
|
// create class proxy, in case a patch needs mutability
|
||||||
val classProxy = ClassProxy(classDef, index)
|
val classProxy = ClassProxy(classDef, index)
|
||||||
methodMap[signature.name] = SignatureResolverResult(
|
signature.result = SignatureResolverResult(
|
||||||
classProxy,
|
classProxy,
|
||||||
patternScanData,
|
patternScanData,
|
||||||
method.name,
|
method.name,
|
||||||
@ -89,8 +86,8 @@ internal class SignatureResolver(
|
|||||||
val pattern = signature.opcodes!!
|
val pattern = signature.opcodes!!
|
||||||
val size = pattern.count()
|
val size = pattern.count()
|
||||||
var threshold = 0
|
var threshold = 0
|
||||||
if (signature.metadata.patcher.resolverMethod is ResolverMethod.Fuzzy) {
|
if (signature.methodSignatureMetadata.patternScanMethod is PatternScanMethod.Fuzzy) {
|
||||||
threshold = signature.metadata.patcher.resolverMethod.threshold
|
threshold = signature.methodSignatureMetadata.patternScanMethod.threshold
|
||||||
}
|
}
|
||||||
|
|
||||||
for (instructionIndex in 0 until count) {
|
for (instructionIndex in 0 until count) {
|
||||||
|
@ -1,190 +0,0 @@
|
|||||||
package app.revanced.patcher
|
|
||||||
|
|
||||||
import app.revanced.patcher.cache.Cache
|
|
||||||
import app.revanced.patcher.extensions.AccessFlagExtensions.Companion.or
|
|
||||||
import app.revanced.patcher.extensions.addInstructions
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
import app.revanced.patcher.patch.PatchMetadata
|
|
||||||
import app.revanced.patcher.patch.PatchResult
|
|
||||||
import app.revanced.patcher.patch.PatchResultSuccess
|
|
||||||
import app.revanced.patcher.proxy.mutableTypes.MutableField.Companion.toMutable
|
|
||||||
import app.revanced.patcher.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
|
||||||
import app.revanced.patcher.signature.*
|
|
||||||
import app.revanced.patcher.smali.asInstruction
|
|
||||||
import app.revanced.patcher.smali.asInstructions
|
|
||||||
import com.google.common.collect.ImmutableList
|
|
||||||
import org.jf.dexlib2.AccessFlags
|
|
||||||
import org.jf.dexlib2.Opcode
|
|
||||||
import org.jf.dexlib2.builder.instruction.BuilderInstruction11x
|
|
||||||
import org.jf.dexlib2.builder.instruction.BuilderInstruction21c
|
|
||||||
import org.jf.dexlib2.immutable.ImmutableField
|
|
||||||
import org.jf.dexlib2.immutable.ImmutableMethod
|
|
||||||
import org.jf.dexlib2.immutable.ImmutableMethodImplementation
|
|
||||||
import org.jf.dexlib2.immutable.reference.ImmutableFieldReference
|
|
||||||
import org.jf.dexlib2.immutable.reference.ImmutableStringReference
|
|
||||||
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
internal class PatcherTest {
|
|
||||||
companion object {
|
|
||||||
val testSignatures = listOf(
|
|
||||||
MethodSignature(
|
|
||||||
"main-method",
|
|
||||||
SignatureMetadata(
|
|
||||||
method = MethodMetadata(
|
|
||||||
definingClass = "TestClass",
|
|
||||||
methodName = "main",
|
|
||||||
comment = "Main method of TestClass. Version 1.0.0"
|
|
||||||
),
|
|
||||||
patcher = PatcherMetadata(
|
|
||||||
resolverMethod = ResolverMethod.Fuzzy(2)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"V",
|
|
||||||
AccessFlags.PUBLIC or AccessFlags.STATIC or AccessFlags.STATIC,
|
|
||||||
listOf("[L"),
|
|
||||||
listOf(
|
|
||||||
Opcode.CONST_STRING,
|
|
||||||
Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver.
|
|
||||||
Opcode.RETURN_VOID
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testPatcher() {
|
|
||||||
val patcher = Patcher(
|
|
||||||
File(PatcherTest::class.java.getResource("/test1.dex")!!.toURI()),
|
|
||||||
testSignatures
|
|
||||||
)
|
|
||||||
|
|
||||||
patcher.addPatches(listOf(
|
|
||||||
object : Patch(PatchMetadata(
|
|
||||||
"test-patch",
|
|
||||||
"My Test Patch",
|
|
||||||
"A very good description."
|
|
||||||
)) {
|
|
||||||
override fun execute(cache: Cache): PatchResult {
|
|
||||||
// Get the result from the resolver cache
|
|
||||||
val result = cache.methodMap["main-method"]
|
|
||||||
// Get the implementation for the resolved method
|
|
||||||
val implementation = result.method.implementation!!
|
|
||||||
// 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 instruction with the opcode CONST_STRING.
|
|
||||||
val startIndex = result.scanData.startIndex
|
|
||||||
|
|
||||||
// Replace the instruction at index startIndex with a new instruction.
|
|
||||||
// The instruction format can be found in the docs at
|
|
||||||
// https://source.android.com/devices/tech/dalvik/dalvik-bytecode
|
|
||||||
//
|
|
||||||
// In our case we want an instruction with the opcode CONST_STRING
|
|
||||||
// and the string "Hello, ReVanced! Adding bytecode.".
|
|
||||||
// The format is 21c, so we create a new BuilderInstruction21c
|
|
||||||
// This instruction will hold the string reference constant in the virtual register 1.
|
|
||||||
// For that a reference to the string is needed. It can be created with an ImmutableStringReference.
|
|
||||||
// At last, use the method replaceInstruction to replace it at the given index startIndex.
|
|
||||||
implementation.replaceInstruction(
|
|
||||||
startIndex,
|
|
||||||
BuilderInstruction21c(
|
|
||||||
Opcode.CONST_STRING,
|
|
||||||
1,
|
|
||||||
ImmutableStringReference("Hello, ReVanced! Editing bytecode.")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get the class in which the method matching our signature is defined in.
|
|
||||||
val mainClass = cache.findClass {
|
|
||||||
it.type == result.definingClassProxy.immutableClass.type
|
|
||||||
}!!.resolve()
|
|
||||||
|
|
||||||
// Add a new method returning a string
|
|
||||||
mainClass.methods.add(
|
|
||||||
ImmutableMethod(
|
|
||||||
result.definingClassProxy.immutableClass.type,
|
|
||||||
"returnHello",
|
|
||||||
null,
|
|
||||||
"Ljava/lang/String;",
|
|
||||||
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ImmutableMethodImplementation(
|
|
||||||
1,
|
|
||||||
ImmutableList.of(
|
|
||||||
BuilderInstruction21c(
|
|
||||||
Opcode.CONST_STRING,
|
|
||||||
0,
|
|
||||||
ImmutableStringReference("Hello, ReVanced! Adding bytecode.")
|
|
||||||
),
|
|
||||||
BuilderInstruction11x(Opcode.RETURN_OBJECT, 0)
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
).toMutable()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add a field in the main class
|
|
||||||
// We will use this field in our method below to call println on
|
|
||||||
// The field holds the Ljava/io/PrintStream->out; field
|
|
||||||
mainClass.fields.add(
|
|
||||||
ImmutableField(
|
|
||||||
mainClass.type,
|
|
||||||
"dummyField",
|
|
||||||
"Ljava/io/PrintStream;",
|
|
||||||
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
|
||||||
ImmutableFieldEncodedValue(
|
|
||||||
ImmutableFieldReference(
|
|
||||||
"Ljava/lang/System;",
|
|
||||||
"out",
|
|
||||||
"Ljava/io/PrintStream;"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
).toMutable()
|
|
||||||
)
|
|
||||||
|
|
||||||
// store the fields initial value into the first virtual register
|
|
||||||
implementation.replaceInstruction(
|
|
||||||
0,
|
|
||||||
"sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;".asInstruction()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Now let's create a new call to our method and print the return value!
|
|
||||||
// You can also use the smali compiler to create instructions.
|
|
||||||
// For this sake of example I reuse the TestClass field dummyField inside the virtual register 0.
|
|
||||||
//
|
|
||||||
// Control flow instructions are not supported as of now.
|
|
||||||
val instructions = """
|
|
||||||
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
|
|
||||||
move-result-object v1
|
|
||||||
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
|
||||||
""".trimIndent().asInstructions()
|
|
||||||
implementation.addInstructions(startIndex + 2, instructions)
|
|
||||||
|
|
||||||
// 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()!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val out = patcher.save()
|
|
||||||
assertTrue(out.isNotEmpty(), "Expected the output of Patcher#save() to not be empty.")
|
|
||||||
}
|
|
||||||
}
|
|
190
src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt
Normal file
190
src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
package app.revanced.patcher.usage
|
||||||
|
|
||||||
|
import app.revanced.patcher.PatcherData
|
||||||
|
import app.revanced.patcher.extensions.AccessFlagExtensions.Companion.or
|
||||||
|
import app.revanced.patcher.extensions.addInstructions
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchMetadata
|
||||||
|
import app.revanced.patcher.patch.PatchResult
|
||||||
|
import app.revanced.patcher.patch.PatchResultSuccess
|
||||||
|
import app.revanced.patcher.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||||
|
import app.revanced.patcher.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
|
import app.revanced.patcher.signature.MethodMetadata
|
||||||
|
import app.revanced.patcher.signature.MethodSignature
|
||||||
|
import app.revanced.patcher.signature.MethodSignatureMetadata
|
||||||
|
import app.revanced.patcher.signature.PatternScanMethod
|
||||||
|
import app.revanced.patcher.smali.asInstruction
|
||||||
|
import app.revanced.patcher.smali.asInstructions
|
||||||
|
import com.google.common.collect.ImmutableList
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.Format
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.builder.instruction.BuilderInstruction11x
|
||||||
|
import org.jf.dexlib2.builder.instruction.BuilderInstruction21c
|
||||||
|
import org.jf.dexlib2.iface.instruction.formats.Instruction21c
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableField
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableMethod
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.immutable.reference.ImmutableFieldReference
|
||||||
|
import org.jf.dexlib2.immutable.reference.ImmutableStringReference
|
||||||
|
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
||||||
|
import org.jf.dexlib2.util.Preconditions
|
||||||
|
|
||||||
|
class ExamplePatch : Patch(
|
||||||
|
metadata = PatchMetadata(
|
||||||
|
shortName = "example-patch",
|
||||||
|
name = "ReVanced example patch",
|
||||||
|
description = "A demonstrative patch to feature the core features of the ReVanced patcher",
|
||||||
|
compatiblePackages = arrayOf("com.example.examplePackage"),
|
||||||
|
version = "0.0.1"
|
||||||
|
),
|
||||||
|
signatures = setOf(
|
||||||
|
MethodSignature(
|
||||||
|
MethodSignatureMetadata(
|
||||||
|
name = "Example signature",
|
||||||
|
methodMetadata = MethodMetadata(
|
||||||
|
definingClass = "TestClass",
|
||||||
|
name = "main",
|
||||||
|
),
|
||||||
|
patternScanMethod = PatternScanMethod.Fuzzy(2),
|
||||||
|
compatiblePackages = arrayOf("com.example.examplePackage"),
|
||||||
|
description = "The main method of TestClass",
|
||||||
|
version = "1.0.0"
|
||||||
|
),
|
||||||
|
returnType = "V",
|
||||||
|
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC or AccessFlags.STATIC,
|
||||||
|
methodParameters = listOf("[L"),
|
||||||
|
opcodes = listOf(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver.
|
||||||
|
Opcode.RETURN_VOID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// This function will be executed by the patcher.
|
||||||
|
// You can treat it as a constructor
|
||||||
|
override fun execute(patcherData: PatcherData): PatchResult {
|
||||||
|
|
||||||
|
// Get the resolved method for the signature from the resolver cache
|
||||||
|
val result = signatures.first().result!!
|
||||||
|
|
||||||
|
// Get the implementation for the resolved method
|
||||||
|
val implementation = result.method.implementation!!
|
||||||
|
|
||||||
|
// 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 instruction with the opcode CONST_STRING.
|
||||||
|
val startIndex = result.scanData.startIndex
|
||||||
|
|
||||||
|
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
|
||||||
|
|
||||||
|
// Get the class in which the method matching our signature is defined in.
|
||||||
|
val mainClass = patcherData.findClass {
|
||||||
|
it.type == result.definingClassProxy.immutableClass.type
|
||||||
|
}!!.resolve()
|
||||||
|
|
||||||
|
// Add a new method returning a string
|
||||||
|
mainClass.methods.add(
|
||||||
|
ImmutableMethod(
|
||||||
|
result.definingClassProxy.immutableClass.type,
|
||||||
|
"returnHello",
|
||||||
|
null,
|
||||||
|
"Ljava/lang/String;",
|
||||||
|
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ImmutableMethodImplementation(
|
||||||
|
1,
|
||||||
|
ImmutableList.of(
|
||||||
|
BuilderInstruction21c(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
0,
|
||||||
|
ImmutableStringReference("Hello, ReVanced! Adding bytecode.")
|
||||||
|
),
|
||||||
|
BuilderInstruction11x(Opcode.RETURN_OBJECT, 0)
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
).toMutable()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add a field in the main class
|
||||||
|
// We will use this field in our method below to call println on
|
||||||
|
// The field holds the Ljava/io/PrintStream->out; field
|
||||||
|
mainClass.fields.add(
|
||||||
|
ImmutableField(
|
||||||
|
mainClass.type,
|
||||||
|
"dummyField",
|
||||||
|
"Ljava/io/PrintStream;",
|
||||||
|
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
||||||
|
ImmutableFieldEncodedValue(
|
||||||
|
ImmutableFieldReference(
|
||||||
|
"Ljava/lang/System;",
|
||||||
|
"out",
|
||||||
|
"Ljava/io/PrintStream;"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
).toMutable()
|
||||||
|
)
|
||||||
|
|
||||||
|
// store the fields initial value into the first virtual register
|
||||||
|
implementation.replaceInstruction(
|
||||||
|
0,
|
||||||
|
"sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;".asInstruction()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now let's create a new call to our method and print the return value!
|
||||||
|
// You can also use the smali compiler to create instructions.
|
||||||
|
// For this sake of example I reuse the TestClass field dummyField inside the virtual register 0.
|
||||||
|
//
|
||||||
|
// Control flow instructions are not supported as of now.
|
||||||
|
val instructions = """
|
||||||
|
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
|
||||||
|
move-result-object v1
|
||||||
|
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||||
|
""".trimIndent().asInstructions()
|
||||||
|
implementation.addInstructions(startIndex + 2, instructions)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the string for an instruction at the given index with a new one.
|
||||||
|
* @param index The index of the instruction to replace the string for
|
||||||
|
* @param string The replacing string
|
||||||
|
*/
|
||||||
|
private fun MutableMethodImplementation.replaceStringAt(index: Int, string: String) {
|
||||||
|
val instruction = this.instructions[index]
|
||||||
|
|
||||||
|
// Utility method of dexlib2
|
||||||
|
Preconditions.checkFormat(instruction.opcode, Format.Format21c)
|
||||||
|
|
||||||
|
// Cast this to an instruction of the format 21c
|
||||||
|
// The instruction format can be found in the docs at
|
||||||
|
// https://source.android.com/devices/tech/dalvik/dalvik-bytecode
|
||||||
|
val strInstruction = instruction as Instruction21c
|
||||||
|
|
||||||
|
// In our case we want an instruction with the opcode CONST_STRING
|
||||||
|
// The format is 21c, so we create a new BuilderInstruction21c
|
||||||
|
// This instruction will hold the string reference constant in the virtual register of the original instruction
|
||||||
|
// For that a reference to the string is needed. It can be created with an ImmutableStringReference.
|
||||||
|
// At last, use the method replaceInstruction to replace it at the given index startIndex.
|
||||||
|
this.replaceInstruction(
|
||||||
|
index,
|
||||||
|
BuilderInstruction21c(
|
||||||
|
Opcode.CONST_STRING,
|
||||||
|
strInstruction.registerA,
|
||||||
|
ImmutableStringReference(string)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user