1
mirror of https://github.com/revanced/revanced-patcher synced 2024-11-27 18:13:37 +01:00

chore: merge branch dev to main (#191)

This commit is contained in:
oSumAtrIX 2023-06-27 04:31:09 +02:00 committed by GitHub
commit a379b69eeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 333 additions and 94 deletions

View File

@ -1,3 +1,32 @@
## [11.0.2-dev.4](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.3...v11.0.2-dev.4) (2023-06-27)
### Bug Fixes
* do not load annotations as patches ([519359a](https://github.com/revanced/revanced-patcher/commit/519359a9eb0e9dfa390c5016e9fe4a7490b8ab18))
## [11.0.2-dev.3](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.2...v11.0.2-dev.3) (2023-06-27)
### Performance Improvements
* resolve fingerprints using method maps ([#185](https://github.com/revanced/revanced-patcher/issues/185)) ([d718134](https://github.com/revanced/revanced-patcher/commit/d718134ab26423e02708e01eba711737f9260ba0))
## [11.0.2-dev.2](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.1...v11.0.2-dev.2) (2023-06-18)
### Bug Fixes
* use `versionCode` if `versionName` is unavailable ([6e1b647](https://github.com/revanced/revanced-patcher/commit/6e1b6479b677657c226693e9cc6b63f4ef2ee060))
## [11.0.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.1...v11.0.2-dev.1) (2023-06-14)
### Bug Fixes
* catch exceptions from closing patches ([d5d6f85](https://github.com/revanced/revanced-patcher/commit/d5d6f85084c03ed9c776632823ca12394a716167))
* only close succeeded patches ([b8151eb](https://github.com/revanced/revanced-patcher/commit/b8151ebccb5b27dd9e06fa63235cf9baeef1c0ee))
## [11.0.1](https://github.com/revanced/revanced-patcher/compare/v11.0.0...v11.0.1) (2023-06-12)

View File

@ -1,2 +1,2 @@
kotlin.code.style = official
version = 11.0.1
version = 11.0.2-dev.4

View File

@ -4,7 +4,8 @@ import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
import app.revanced.patcher.patch.*
import app.revanced.patcher.util.VersionReader
import brut.androlib.Androlib
@ -23,6 +24,7 @@ import lanchon.multidexlib2.MultiDexIO
import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.iface.DexFile
import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.Closeable
import java.io.File
import java.io.OutputStream
import java.nio.file.Files
@ -248,6 +250,7 @@ class Patcher(private val options: PatcherOptions) {
axmlParser, AndrolibResources().resXmlSerializer
).decodeManifest(
extInputFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) {
// do nothing
@ -260,7 +263,7 @@ class Patcher(private val options: PatcherOptions) {
// read of the resourceTable which is created by reading the manifest file
context.packageMetadata.let { metadata ->
metadata.packageName = resourceTable.currentResPackage.name
metadata.packageVersion = resourceTable.versionInfo.versionName
metadata.packageVersion = resourceTable.versionInfo.versionName ?: resourceTable.versionInfo.versionCode
metadata.metaInfo.versionInfo = resourceTable.versionInfo
metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
}
@ -320,10 +323,7 @@ class Patcher(private val options: PatcherOptions) {
context.resourceContext
} else {
context.bytecodeContext.also { context ->
(patchInstance as BytecodePatch).fingerprints?.resolve(
context,
context.classes.classes
)
(patchInstance as BytecodePatch).fingerprints?.resolveUsingLookupMap(context)
}
}
@ -343,31 +343,52 @@ class Patcher(private val options: PatcherOptions) {
return sequence {
if (mergeIntegrations) context.integrations.merge(logger)
logger.trace("Initialize lookup maps for method MethodFingerprint resolution")
MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext)
// prevent from decoding the manifest twice if it is not needed
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
logger.trace("Executing all patches")
logger.info("Executing patches")
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // first is name
try {
context.patches.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
context.patches.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
val result = if (patchResult.isSuccess()) {
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
val result = if (patchResult.isSuccess()) {
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) return@sequence
}
} finally {
executedPatches.values.reversed().forEach { (patch, _) ->
patch.close()
}
// TODO: This prints before the patch really finishes in case it is a Closeable
// because the Closeable is closed after all patches are executed.
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) return@sequence
}
executedPatches.values
.filter(ExecutedPatch::success)
.map(ExecutedPatch::patchInstance)
.filterIsInstance(Closeable::class.java)
.asReversed().forEach {
try {
it.close()
} catch (exception: Exception) {
val patchName = (it as Patch<Context>).javaClass.patchName
logger.error("Failed to close '$patchName': ${exception.stackTraceToString()}")
yield(patchName to Result.failure(exception))
// This is not failsafe. If a patch throws an exception while closing,
// the other patches that depend on it may fail.
if (stopOnError) return@sequence
}
}
}
}

View File

@ -4,7 +4,9 @@ import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.Fingerprint
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.patch.PatchResultError
import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
@ -12,19 +14,21 @@ import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.util.MethodUtil
import java.util.*
private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch
private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult
private typealias MethodClassPair = Pair<Method, ClassDef>
/**
* Represents the [MethodFingerprint] for a method.
* @param returnType The return type of the method.
* @param accessFlags The access flags of the method.
* @param parameters The parameters of the method.
* @param opcodes The list of opcodes of the method.
* @param strings A list of strings which a method contains.
* A fingerprint to resolve methods.
*
* @param returnType The method's return type compared using [String.startsWith].
* @param accessFlags The method's exact access flags using values of [AccessFlags].
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @param opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by `null`.
* @param strings A list of the method's strings compared each using [String.contains].
* @param customFingerprint A custom condition for this fingerprint.
* A `null` opcode is equals to an unknown opcode.
*/
abstract class MethodFingerprint(
internal val returnType: String? = null,
@ -40,6 +44,192 @@ abstract class MethodFingerprint(
var result: MethodFingerprintResult? = null
companion object {
/**
* A list of methods and the class they were found in.
*/
private val methods = mutableListOf<MethodClassPair>()
/**
* Lookup map for methods keyed to the methods access flags, return type and parameter.
*/
private val methodSignatureLookupMap = mutableMapOf<String, MutableList<MethodClassPair>>()
/**
* Lookup map for methods keyed to the strings contained in the method.
*/
private val methodStringsLookupMap = mutableMapOf<String, MutableList<MethodClassPair>>()
/**
* Appends a string based on the parameter reference types of this method.
*/
private fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
// Maximum parameters to use in the signature key.
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
// To keep the signature map from becoming needlessly bloated,
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
// The value of 5 was chosen based on local performance testing and is not set in stone.
val maxSignatureParameters = 5
// Must append a unique value before the parameters to distinguish this key includes the parameters.
// If this is not appended, then methods with no parameters
// will collide with different keys that specify access/return but omit the parameters.
append("p:")
parameters.forEachIndexed { index, parameter ->
if (index >= maxSignatureParameters) return
append(parameter.first())
}
}
/**
* Initializes lookup maps for [MethodFingerprint] resolution
* using attributes of methods such as the method signature or strings.
*
* @param context The [BytecodeContext] containing the classes to initialize the lookup maps with.
*/
internal fun initializeFingerprintResolutionLookupMaps(context: BytecodeContext) {
fun MutableMap<String, MutableList<MethodClassPair>>.add(
key: String,
methodClassPair: MethodClassPair
) {
var methodClassPairs = this[key]
methodClassPairs ?: run {
methodClassPairs = LinkedList<MethodClassPair>().also { this[key] = it }
}
methodClassPairs!!.add(methodClassPair)
}
if (methods.isNotEmpty()) throw PatchResultError("Map already initialized")
context.classes.classes.forEach { classDef ->
classDef.methods.forEach { method ->
val methodClassPair = method to classDef
// For fingerprints with no access or return type specified.
methods += methodClassPair
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
// Add <access><returnType> as the key.
methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair)
// Add <access><returnType>[parameters] as the key.
methodSignatureLookupMap.add(
buildString {
append(accessFlagsReturnKey)
appendParameters(method.parameterTypes)
},
methodClassPair
)
// Add strings contained in the method as the key.
method.implementation?.instructions?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO)
return@instructions
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
methodStringsLookupMap.add(string, methodClassPair)
}
// In the future, the class type could be added to the lookup map.
// This would require MethodFingerprint to be changed to include the class type.
}
}
}
/**
* Resolve a list of [MethodFingerprint] using the lookup map built by [initializeFingerprintResolutionLookupMaps].
*
* [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable
* amount of time because they are resolved in sequence.
*
* For apps with many fingerprints, resolving performance can be improved by:
* - Slowest: Specify [opcodes] and nothing else.
* - Fast: Specify [accessFlags], [returnType].
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*/
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
if (methods.isEmpty()) throw PatchResultError("lookup map not initialized")
for (fingerprint in this) {
fingerprint.resolveUsingLookupMap(context)
}
}
/**
* Resolve a [MethodFingerprint] using the lookup map built by [initializeFingerprintResolutionLookupMaps].
*
* [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable
* amount of time because they are resolved in sequence.
*
* For apps with many fingerprints, resolving performance can be improved by:
* - Slowest: Specify [opcodes] and nothing else.
* - Fast: Specify [accessFlags], [returnType].
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*/
internal fun MethodFingerprint.resolveUsingLookupMap(context: BytecodeContext): Boolean {
/**
* Lookup [MethodClassPair]s that match the methods strings present in a [MethodFingerprint].
*
* @return A list of [MethodClassPair]s that match the methods strings present in a [MethodFingerprint].
*/
fun MethodFingerprint.methodStringsLookup(): List<MethodClassPair>? {
strings?.forEach {
val methods = methodStringsLookupMap[it]
if (methods != null) return methods
}
return null
}
/**
* Lookup [MethodClassPair]s that match the method signature present in a [MethodFingerprint].
*
* @return A list of [MethodClassPair]s that match the method signature present in a [MethodFingerprint].
*/
fun MethodFingerprint.methodSignatureLookup(): List<MethodClassPair> {
if (accessFlags == null) return methods
var returnTypeValue = returnType
if (returnTypeValue == null) {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
// Constructors always have void return type
returnTypeValue = "V"
} else {
return methods
}
}
val key = buildString {
append(accessFlags)
append(returnTypeValue.first())
if (parameters != null) appendParameters(parameters)
}
return methodSignatureLookupMap[key]!!
}
/**
* Resolve a [MethodFingerprint] using a list of [MethodClassPair].
*
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolveUsingMethodClassPair(classMethods: Iterable<MethodClassPair>): Boolean {
classMethods.forEach { classAndMethod ->
if (resolve(context, classAndMethod.first, classAndMethod.second)) return true
}
return false
}
val methodsWithSameStrings = methodStringsLookup()
if (methodsWithSameStrings != null) if (resolveUsingMethodClassPair(methodsWithSameStrings)) return true
// No strings declared or none matched (partial matches are allowed).
// Use signature matching.
return resolveUsingMethodClassPair(methodSignatureLookup())
}
/**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
*
@ -48,10 +238,10 @@ abstract class MethodFingerprint(
* @return True if the resolution was successful, false otherwise.
*/
fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) {
for (fingerprint in this) // For each fingerprint
classes@ for (classDef in classes) // search through all classes for the fingerprint
for (fingerprint in this) // For each fingerprint...
classes@ for (classDef in classes) // ...search through all classes for the MethodFingerprint
if (fingerprint.resolve(context, classDef))
break@classes // if the resolution succeeded, continue with the next fingerprint
break@classes // ...if the resolution succeeded, continue with the next MethodFingerprint.
}
/**
@ -144,6 +334,52 @@ abstract class MethodFingerprint(
val patternScanResult = if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false
fun Method.patternScan(
fingerprint: MethodFingerprint
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.createWarnings(pattern, instructions)
return result
}
}
return null
}
method.patternScan(methodFingerprint) ?: return false
} else null
@ -160,52 +396,6 @@ abstract class MethodFingerprint(
return true
}
private fun Method.patternScan(
fingerprint: MethodFingerprint
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.createWarnings(pattern, instructions)
return result
}
}
return null
}
private fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.createWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
) = buildList {

View File

@ -12,7 +12,7 @@ import java.io.Closeable
* If it implements [Closeable], it will be closed after all patches have been executed.
* Closing will be done in reverse execution order.
*/
sealed interface Patch<out T : Context> : Closeable {
sealed interface Patch<out T : Context> {
/**
* The main function of the [Patch] which the patcher will call.
*
@ -20,13 +20,6 @@ sealed interface Patch<out T : Context> : Closeable {
* @return The result of executing the patch.
*/
fun execute(context: @UnsafeVariance T): PatchResult
/**
* The closing function for this patch.
*
* This can be treated like popping the patch from the current patch stack.
*/
override fun close() {}
}
/**

View File

@ -3,6 +3,7 @@
package app.revanced.patcher.util.patch
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import org.jf.dexlib2.DexFileFactory
@ -19,7 +20,13 @@ sealed class PatchBundle(path: String) : File(path) {
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
classNames.forEach { className ->
val clazz = classLoader.loadClass(className)
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) return@forEach
// Annotations can not Patch.
if (clazz.isAnnotation) return@forEach
clazz.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
?: return@forEach
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Context>>)
}
}.sortedBy { it.patchName }
@ -43,11 +50,9 @@ sealed class PatchBundle(path: String) : File(path) {
),
JarFile(this)
.stream()
.filter {it.name.endsWith(".class") && !it.name.contains("$")}
.map({it -> it.realName.replace('/', '.').replace(".class", "")}).iterator()
.filter { it.name.endsWith(".class") && !it.name.contains("$") }
.map { it.realName.replace('/', '.').replace(".class", "") }.iterator()
)
}
/**
@ -63,8 +68,9 @@ sealed class PatchBundle(path: String) : File(path) {
* Patches will be loaded to the provided [dexClassLoader].
*/
fun loadPatches() = loadPatches(dexClassLoader,
DexFileFactory.loadDexFile(path, null).classes.asSequence().map({ classDef ->
DexFileFactory.loadDexFile(path, null).classes.asSequence().map { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
}).iterator())
}.iterator()
)
}
}