mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-26 04:31:03 +00:00
feat: Convert APIs to Kotlin DSL (#298)
This commit converts various APIs to Kotlin DSL. BREAKING CHANGE: Various old APIs are removed, and DSL APIs are added instead.
This commit is contained in:
467
src/main/kotlin/app/revanced/patcher/Fingerprint.kt
Normal file
467
src/main/kotlin/app/revanced/patcher/Fingerprint.kt
Normal file
@@ -0,0 +1,467 @@
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
|
||||
import app.revanced.patcher.patch.BytecodePatchBuilder
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps.Companion.appendParameters
|
||||
import app.revanced.patcher.patch.MethodClassPairs
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
/**
|
||||
* A fingerprint.
|
||||
*
|
||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
||||
* @param returnType The return type. Compared using [String.startsWith].
|
||||
* @param parameters The parameters. Partial matches allowed and follow the same rules as [returnType].
|
||||
* @param opcodes A pattern of instruction opcodes. `null` can be used as a wildcard.
|
||||
* @param strings A list of the strings. Compared using [String.contains].
|
||||
* @param custom A custom condition for this fingerprint.
|
||||
* @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern.
|
||||
*/
|
||||
class Fingerprint internal constructor(
|
||||
internal val accessFlags: Int?,
|
||||
internal val returnType: String?,
|
||||
internal val parameters: List<String>?,
|
||||
internal val opcodes: List<Opcode?>?,
|
||||
internal val strings: List<String>?,
|
||||
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
|
||||
private val fuzzyPatternScanThreshold: Int,
|
||||
) {
|
||||
/**
|
||||
* The match for this [Fingerprint]. Null if unmatched.
|
||||
*/
|
||||
var match: Match? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Match using [BytecodePatchContext.LookupMaps].
|
||||
*
|
||||
* Generally faster than the other [match] overloads when there are many methods to check for a match.
|
||||
*
|
||||
* Fingerprints can be optimized for performance:
|
||||
* - Slowest: Specify [custom] or [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.
|
||||
*
|
||||
* @param context The context to create mutable proxies for the matched method and its class.
|
||||
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
|
||||
*/
|
||||
internal fun match(context: BytecodePatchContext): Boolean {
|
||||
val lookupMaps = context.lookupMaps
|
||||
|
||||
fun Fingerprint.match(methodClasses: MethodClassPairs): Boolean {
|
||||
methodClasses.forEach { (classDef, method) ->
|
||||
if (match(context, classDef, method)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: If only one string is necessary, why not use a single string for every fingerprint?
|
||||
fun Fingerprint.lookupByStrings() = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }
|
||||
if (lookupByStrings()?.let(::match) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// No strings declared or none matched (partial matches are allowed).
|
||||
// Use signature matching.
|
||||
fun Fingerprint.lookupBySignature(): MethodClassPairs {
|
||||
if (accessFlags == null) return lookupMaps.allMethods
|
||||
|
||||
var returnTypeValue = returnType
|
||||
if (returnTypeValue == null) {
|
||||
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
|
||||
// Constructors always have void return type.
|
||||
returnTypeValue = "V"
|
||||
} else {
|
||||
return lookupMaps.allMethods
|
||||
}
|
||||
}
|
||||
|
||||
val signature =
|
||||
buildString {
|
||||
append(accessFlags)
|
||||
append(returnTypeValue.first())
|
||||
appendParameters(parameters ?: return@buildString)
|
||||
}
|
||||
|
||||
return lookupMaps.methodsBySignature[signature] ?: return MethodClassPairs()
|
||||
}
|
||||
return match(lookupBySignature())
|
||||
}
|
||||
|
||||
/**
|
||||
* Match using a [ClassDef].
|
||||
*
|
||||
* @param classDef The class to match against.
|
||||
* @param context The context to create mutable proxies for the matched method and its class.
|
||||
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
|
||||
*/
|
||||
fun match(
|
||||
context: BytecodePatchContext,
|
||||
classDef: ClassDef,
|
||||
): Boolean {
|
||||
for (method in classDef.methods) {
|
||||
if (match(context, method, classDef)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Match using a [Method].
|
||||
* The class is retrieved from the method.
|
||||
*
|
||||
* @param method The method to match against.
|
||||
* @param context The context to create mutable proxies for the matched method and its class.
|
||||
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
|
||||
*/
|
||||
fun match(
|
||||
context: BytecodePatchContext,
|
||||
method: Method,
|
||||
) = match(context, method, context.classByType(method.definingClass)!!.immutableClass)
|
||||
|
||||
/**
|
||||
* Match using a [Method].
|
||||
*
|
||||
* @param method The method to match against.
|
||||
* @param classDef The class the method is a member of.
|
||||
* @param context The context to create mutable proxies for the matched method and its class.
|
||||
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
|
||||
*/
|
||||
internal fun match(
|
||||
context: BytecodePatchContext,
|
||||
method: Method,
|
||||
classDef: ClassDef,
|
||||
): Boolean {
|
||||
if (match != null) return true
|
||||
|
||||
if (returnType != null && !method.returnType.startsWith(returnType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (accessFlags != null && accessFlags != method.accessFlags) {
|
||||
return false
|
||||
}
|
||||
|
||||
fun parametersEqual(
|
||||
parameters1: Iterable<CharSequence>,
|
||||
parameters2: Iterable<CharSequence>,
|
||||
): Boolean {
|
||||
if (parameters1.count() != parameters2.count()) return false
|
||||
val iterator1 = parameters1.iterator()
|
||||
parameters2.forEach {
|
||||
if (!it.startsWith(iterator1.next())) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO: parseParameters()
|
||||
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (custom != null && !custom.invoke(method, classDef)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val stringMatches: List<Match.StringMatch>? =
|
||||
if (strings != null) {
|
||||
buildList {
|
||||
val instructions = method.instructionsOrNull ?: return false
|
||||
|
||||
val stringsList = strings.toMutableList()
|
||||
|
||||
instructions.forEachIndexed { instructionIndex, instruction ->
|
||||
if (
|
||||
instruction.opcode != Opcode.CONST_STRING &&
|
||||
instruction.opcode != Opcode.CONST_STRING_JUMBO
|
||||
) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
|
||||
val index = stringsList.indexOfFirst(string::contains)
|
||||
if (index == -1) return@forEachIndexed
|
||||
|
||||
add(Match.StringMatch(string, instructionIndex))
|
||||
stringsList.removeAt(index)
|
||||
}
|
||||
|
||||
if (stringsList.isNotEmpty()) return false
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val patternMatch = if (opcodes != null) {
|
||||
val instructions = method.instructionsOrNull ?: return false
|
||||
|
||||
fun patternScan(): Match.PatternMatch? {
|
||||
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
|
||||
|
||||
val instructionLength = instructions.count()
|
||||
val patternLength = opcodes.size
|
||||
|
||||
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 = opcodes.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 entire pattern has been scanned.
|
||||
return Match.PatternMatch(
|
||||
index,
|
||||
index + patternIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
patternScan() ?: return false
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
match = Match(
|
||||
method,
|
||||
classDef,
|
||||
patternMatch,
|
||||
stringMatches,
|
||||
context,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A match for a [Fingerprint].
|
||||
*
|
||||
* @param method The matching method.
|
||||
* @param classDef The class the matching method is a member of.
|
||||
* @param patternMatch The match for the opcode pattern.
|
||||
* @param stringMatches The matches for the strings.
|
||||
* @param context The context to create mutable proxies in.
|
||||
*/
|
||||
class Match(
|
||||
val method: Method,
|
||||
val classDef: ClassDef,
|
||||
val patternMatch: PatternMatch?,
|
||||
val stringMatches: List<StringMatch>?,
|
||||
internal val context: BytecodePatchContext,
|
||||
) {
|
||||
/**
|
||||
* The mutable version of [classDef].
|
||||
*
|
||||
* Accessing this property allocates a [ClassProxy].
|
||||
* Use [classDef] if mutable access is not required.
|
||||
*/
|
||||
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
||||
|
||||
/**
|
||||
* The mutable version of [method].
|
||||
*
|
||||
* Accessing this property allocates a [ClassProxy].
|
||||
* Use [method] if mutable access is not required.
|
||||
*/
|
||||
val mutableMethod by lazy { mutableClass.methods.first { MethodUtil.methodSignaturesMatch(it, method) } }
|
||||
|
||||
/**
|
||||
* A match for an opcode pattern.
|
||||
* @param startIndex The index of the first opcode of the pattern in the method.
|
||||
* @param endIndex The index of the last opcode of the pattern in the method.
|
||||
*/
|
||||
class PatternMatch(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* A match for a string.
|
||||
*
|
||||
* @param string The string that matched.
|
||||
* @param index The index of the instruction in the method.
|
||||
*/
|
||||
class StringMatch(val string: String, val index: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for [Fingerprint].
|
||||
*
|
||||
* @property accessFlags The exact access flags using values of [AccessFlags].
|
||||
* @property returnType The return type compared using [String.startsWith].
|
||||
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
|
||||
* @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
|
||||
* @property strings A list of the strings compared each using [String.contains].
|
||||
* @property customBlock A custom condition for this fingerprint.
|
||||
* @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning.
|
||||
*
|
||||
* @constructor Create a new [FingerprintBuilder].
|
||||
*/
|
||||
class FingerprintBuilder internal constructor(
|
||||
private val fuzzyPatternScanThreshold: Int = 0,
|
||||
) {
|
||||
private var accessFlags: Int? = null
|
||||
private var returnType: String? = null
|
||||
private var parameters: List<String>? = null
|
||||
private var opcodes: List<Opcode?>? = null
|
||||
private var strings: List<String>? = null
|
||||
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
|
||||
|
||||
/**
|
||||
* Set the access flags.
|
||||
*
|
||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
||||
*/
|
||||
fun accessFlags(accessFlags: Int) {
|
||||
this.accessFlags = accessFlags
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the access flags.
|
||||
*
|
||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
||||
*/
|
||||
fun accessFlags(vararg accessFlags: AccessFlags) {
|
||||
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the return type.
|
||||
*
|
||||
* @param returnType The return type compared using [String.startsWith].
|
||||
*/
|
||||
infix fun returns(returnType: String) {
|
||||
this.returnType = returnType
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parameters.
|
||||
*
|
||||
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
|
||||
*/
|
||||
fun parameters(vararg parameters: String) {
|
||||
this.parameters = parameters.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the opcodes.
|
||||
*
|
||||
* @param opcodes An opcode pattern of instructions.
|
||||
* Wildcard or unknown opcodes can be specified by `null`.
|
||||
*/
|
||||
fun opcodes(vararg opcodes: Opcode?) {
|
||||
this.opcodes = opcodes.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the opcodes.
|
||||
*
|
||||
* @param instructions A list of instructions or opcode names in SMALI format.
|
||||
* - Wildcard or unknown opcodes can be specified by `null`.
|
||||
* - Empty lines are ignored.
|
||||
* - Each instruction must be on a new line.
|
||||
* - The opcode name is enough, no need to specify the operands.
|
||||
*
|
||||
* @throws Exception If an unknown opcode is used.
|
||||
*/
|
||||
fun opcodes(instructions: String) {
|
||||
this.opcodes = instructions.trimIndent().split("\n").filter {
|
||||
it.isNotBlank()
|
||||
}.map {
|
||||
// Remove any operands.
|
||||
val name = it.split(" ", limit = 1).first().trim()
|
||||
if (name == "null") return@map null
|
||||
|
||||
opcodesByName[name] ?: throw Exception("Unknown opcode: $name")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the strings.
|
||||
*
|
||||
* @param strings A list of strings compared each using [String.contains].
|
||||
*/
|
||||
fun strings(vararg strings: String) {
|
||||
this.strings = strings.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom condition for this fingerprint.
|
||||
*
|
||||
* @param customBlock A custom condition for this fingerprint.
|
||||
*/
|
||||
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
|
||||
this.customBlock = customBlock
|
||||
}
|
||||
|
||||
internal fun build() = Fingerprint(
|
||||
accessFlags,
|
||||
returnType,
|
||||
parameters,
|
||||
opcodes,
|
||||
strings,
|
||||
customBlock,
|
||||
fuzzyPatternScanThreshold,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val opcodesByName = Opcode.entries.associateBy { it.name }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [Fingerprint].
|
||||
*
|
||||
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
|
||||
* @param block The block to build the [Fingerprint].
|
||||
*
|
||||
* @return The created [Fingerprint].
|
||||
*/
|
||||
fun fingerprint(
|
||||
fuzzyPatternScanThreshold: Int = 0,
|
||||
block: FingerprintBuilder.() -> Unit,
|
||||
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()
|
||||
|
||||
/**
|
||||
* Create a [Fingerprint] and add it to the set of fingerprints.
|
||||
*
|
||||
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
|
||||
* @param block The block to build the [Fingerprint].
|
||||
*
|
||||
* @return The created [Fingerprint].
|
||||
*/
|
||||
fun BytecodePatchBuilder.fingerprint(
|
||||
fuzzyPatternScanThreshold: Int = 0,
|
||||
block: FingerprintBuilder.() -> Unit,
|
||||
) = app.revanced.patcher.fingerprint(
|
||||
fuzzyPatternScanThreshold,
|
||||
block,
|
||||
)() // Invoke to add it.
|
||||
@@ -1,11 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import java.io.File
|
||||
|
||||
@FunctionalInterface
|
||||
interface IntegrationsConsumer {
|
||||
fun acceptIntegrations(integrations: Set<File>)
|
||||
|
||||
@Deprecated("Use acceptIntegrations(Set<File>) instead.")
|
||||
fun acceptIntegrations(integrations: List<File>)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import dalvik.system.DexClassLoader
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarFile
|
||||
import java.util.logging.Logger
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* A set of [Patch]es.
|
||||
*/
|
||||
typealias PatchSet = Set<Patch<*>>
|
||||
|
||||
/**
|
||||
* A [Patch] class.
|
||||
*/
|
||||
typealias PatchClass = KClass<out Patch<*>>
|
||||
|
||||
/**
|
||||
* A loader of [Patch]es from patch bundles.
|
||||
* This will load all [Patch]es from the given patch bundles that have a name.
|
||||
*
|
||||
* @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle.
|
||||
* @param classLoader The [ClassLoader] to use for loading the classes.
|
||||
* @param patchBundles A set of patches to initialize this instance with.
|
||||
*/
|
||||
sealed class PatchBundleLoader private constructor(
|
||||
classLoader: ClassLoader,
|
||||
patchBundles: Array<out File>,
|
||||
getBinaryClassNames: (patchBundle: File) -> List<String>,
|
||||
// This constructor parameter is unfortunately necessary,
|
||||
// so that a reference to the mutable set is present in the constructor to be able to add patches to it.
|
||||
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
|
||||
private val patchSet: MutableSet<Patch<*>> = mutableSetOf(),
|
||||
) : PatchSet by patchSet {
|
||||
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
|
||||
|
||||
init {
|
||||
patchBundles.flatMap(getBinaryClassNames).asSequence().map {
|
||||
classLoader.loadClass(it)
|
||||
}.filter {
|
||||
Patch::class.java.isAssignableFrom(it)
|
||||
}.mapNotNull { patchClass ->
|
||||
patchClass.getInstance(logger, silent = true)
|
||||
}.filter {
|
||||
it.name != null
|
||||
}.let { patches ->
|
||||
patchSet.addAll(patches)
|
||||
}
|
||||
}
|
||||
|
||||
internal companion object Utils {
|
||||
/**
|
||||
* Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used.
|
||||
*
|
||||
* @param logger The [Logger] to use for logging.
|
||||
* @param silent Whether to suppress logging.
|
||||
* @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated.
|
||||
*/
|
||||
internal fun Class<*>.getInstance(
|
||||
logger: Logger,
|
||||
silent: Boolean = false,
|
||||
): Patch<*>? {
|
||||
return try {
|
||||
getField("INSTANCE").get(null)
|
||||
} catch (exception: NoSuchFieldException) {
|
||||
if (!silent) {
|
||||
logger.fine(
|
||||
"Patch class '$name' has no INSTANCE field, therefor not a singleton. " +
|
||||
"Attempting to instantiate it.",
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
getDeclaredConstructor().newInstance()
|
||||
} catch (exception: Exception) {
|
||||
if (!silent) {
|
||||
logger.severe(
|
||||
"Patch class '$name' is not singleton and has no suitable constructor, " +
|
||||
"therefor cannot be instantiated and is ignored.",
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
} as Patch<*>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PatchBundleLoader] for JAR files.
|
||||
*
|
||||
* @param patchBundles The path to patch bundles of JAR format.
|
||||
*/
|
||||
class Jar(vararg patchBundles: File) : PatchBundleLoader(
|
||||
URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()),
|
||||
patchBundles,
|
||||
{ patchBundle ->
|
||||
JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
|
||||
.map { it.name.substringBeforeLast('.').replace('/', '.') }
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* A [PatchBundleLoader] for [Dex] files.
|
||||
*
|
||||
* @param patchBundles The path to patch bundles of DEX format.
|
||||
* @param optimizedDexDirectory The directory to store optimized DEX files in.
|
||||
* This parameter is deprecated and has no effect since API level 26.
|
||||
*/
|
||||
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
|
||||
DexClassLoader(
|
||||
patchBundles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||
optimizedDexDirectory?.absolutePath,
|
||||
null,
|
||||
PatchBundleLoader::class.java.classLoader,
|
||||
),
|
||||
patchBundles,
|
||||
{ patchBundle ->
|
||||
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
|
||||
.map { classDef ->
|
||||
classDef.type.substring(1, classDef.length - 1)
|
||||
}
|
||||
},
|
||||
) {
|
||||
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
|
||||
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.function.Function
|
||||
|
||||
@FunctionalInterface
|
||||
interface PatchExecutorFunction : Function<Boolean, Flow<PatchResult>>
|
||||
@@ -1,13 +1,8 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.PatchBundleLoader.Utils.getInstance
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.fingerprint.LookupMap
|
||||
import app.revanced.patcher.patch.*
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.util.function.Supplier
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
@@ -15,243 +10,149 @@ import java.util.logging.Logger
|
||||
*
|
||||
* @param config The configuration to use for the patcher.
|
||||
*/
|
||||
class Patcher(
|
||||
private val config: PatcherConfig,
|
||||
) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable {
|
||||
private val logger = Logger.getLogger(Patcher::class.java.name)
|
||||
class Patcher(private val config: PatcherConfig) : Closeable {
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
/**
|
||||
* A context for the patcher containing the current state of the patcher.
|
||||
* The context containing the current state of the patcher.
|
||||
*/
|
||||
val context = PatcherContext(config)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Use Patcher(PatcherConfig) instead.")
|
||||
constructor(
|
||||
patcherOptions: PatcherOptions,
|
||||
) : this(
|
||||
PatcherConfig(
|
||||
patcherOptions.inputFile,
|
||||
patcherOptions.resourceCachePath,
|
||||
patcherOptions.aaptBinaryPath,
|
||||
patcherOptions.frameworkFileDirectory,
|
||||
patcherOptions.multithreadingDexFileWriter,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
context.resourceContext.decodeResources(ResourceContext.ResourceMode.NONE)
|
||||
context.resourceContext.decodeResources(ResourcePatchContext.ResourceMode.NONE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add [Patch]es to ReVanced [Patcher].
|
||||
* Add patches.
|
||||
*
|
||||
* @param patches The [Patch]es to add.
|
||||
* @param patches The patches to add.
|
||||
*/
|
||||
@Suppress("NAME_SHADOWING")
|
||||
override fun acceptPatches(patches: PatchSet) {
|
||||
/**
|
||||
* Add dependencies of a [Patch] recursively to [PatcherContext.allPatches].
|
||||
* If a [Patch] is already in [PatcherContext.allPatches], it will not be added again.
|
||||
*/
|
||||
fun PatchClass.putDependenciesRecursively() {
|
||||
if (context.allPatches.contains(this)) return
|
||||
operator fun plusAssign(patches: Set<Patch<*>>) {
|
||||
// Add all patches to the executablePatches set.
|
||||
context.executablePatches += patches
|
||||
|
||||
val dependency = this.java.getInstance(logger)!!
|
||||
context.allPatches[this] = dependency
|
||||
|
||||
dependency.dependencies?.forEach { it.putDependenciesRecursively() }
|
||||
}
|
||||
|
||||
// Add all patches and their dependencies to the context.
|
||||
// Add all patches and their dependencies to the allPatches set.
|
||||
patches.forEach { patch ->
|
||||
context.executablePatches.putIfAbsent(patch::class, patch) ?: run {
|
||||
context.allPatches[patch::class] = patch
|
||||
fun Patch<*>.addRecursively() =
|
||||
also(context.allPatches::add).dependencies.forEach(Patch<*>::addRecursively)
|
||||
|
||||
patch.dependencies?.forEach { it.putDependenciesRecursively() }
|
||||
}
|
||||
patch.addRecursively()
|
||||
}
|
||||
|
||||
// TODO: Detect circular dependencies.
|
||||
|
||||
/**
|
||||
* Returns true if at least one patch or its dependencies matches the given predicate.
|
||||
*
|
||||
* @param predicate The predicate to match.
|
||||
*/
|
||||
fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean =
|
||||
predicate(this) || dependencies?.any { dependency ->
|
||||
context.allPatches[dependency]!!.anyRecursively(predicate)
|
||||
} ?: false
|
||||
predicate(this) || dependencies.any { dependency -> dependency.anyRecursively(predicate) }
|
||||
|
||||
context.allPatches.values.let { patches ->
|
||||
// Determine the resource mode.
|
||||
|
||||
config.resourceMode = if (patches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) {
|
||||
ResourceContext.ResourceMode.FULL
|
||||
} else if (patches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) {
|
||||
ResourceContext.ResourceMode.RAW_ONLY
|
||||
context.allPatches.let { allPatches ->
|
||||
// Check, if what kind of resource mode is required.
|
||||
config.resourceMode = if (allPatches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) {
|
||||
ResourcePatchContext.ResourceMode.FULL
|
||||
} else if (allPatches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) {
|
||||
ResourcePatchContext.ResourceMode.RAW_ONLY
|
||||
} else {
|
||||
ResourceContext.ResourceMode.NONE
|
||||
ResourcePatchContext.ResourceMode.NONE
|
||||
}
|
||||
|
||||
// Determine, if merging integrations is required.
|
||||
for (patch in patches)
|
||||
if (patch.anyRecursively { it.requiresIntegrations }) {
|
||||
context.bytecodeContext.integrations.merge = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add integrations to the [Patcher].
|
||||
* Execute added patches.
|
||||
*
|
||||
* @param integrations The integrations to add. Must be a DEX file or container of DEX files.
|
||||
* @return A flow of [PatchResult]s.
|
||||
*/
|
||||
override fun acceptIntegrations(integrations: Set<File>) {
|
||||
context.bytecodeContext.integrations.addAll(integrations)
|
||||
}
|
||||
operator fun invoke() = flow {
|
||||
fun Patch<*>.execute(
|
||||
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
|
||||
): PatchResult {
|
||||
// If the patch was executed before or failed, return it's the result.
|
||||
executedPatches[this]?.let { patchResult ->
|
||||
patchResult.exception ?: return patchResult
|
||||
|
||||
@Deprecated(
|
||||
"Use acceptIntegrations(Set<File>) instead.",
|
||||
ReplaceWith("acceptIntegrations(integrations.toSet())"),
|
||||
)
|
||||
override fun acceptIntegrations(integrations: List<File>) = acceptIntegrations(integrations.toSet())
|
||||
return PatchResult(this, PatchException("The patch '$this' failed previously"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute [Patch]es that were added to ReVanced [Patcher].
|
||||
*
|
||||
* @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails.
|
||||
* @return A pair of the name of the [Patch] and its [PatchResult].
|
||||
*/
|
||||
override fun apply(returnOnError: Boolean) =
|
||||
flow {
|
||||
/**
|
||||
* Execute a [Patch] and its dependencies recursively.
|
||||
*
|
||||
* @param patch The [Patch] to execute.
|
||||
* @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
|
||||
* @return The result of executing the [Patch].
|
||||
*/
|
||||
fun executePatch(
|
||||
patch: Patch<*>,
|
||||
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
|
||||
): PatchResult {
|
||||
val patchName = patch.toString()
|
||||
|
||||
executedPatches[patch]?.let { patchResult ->
|
||||
patchResult.exception ?: return patchResult
|
||||
|
||||
// Return a new result with an exception indicating that the patch was not executed previously,
|
||||
// because it is a dependency of another patch that failed.
|
||||
return PatchResult(patch, PatchException("'$patchName' did not succeed previously"))
|
||||
// Recursively execute all dependency patches.
|
||||
dependencies.forEach { dependency ->
|
||||
dependency.execute(executedPatches).exception?.let {
|
||||
return PatchResult(
|
||||
this,
|
||||
PatchException(
|
||||
"The patch \"$this\" depends on \"$dependency\", which raised an exception:\n${it.stackTraceToString()}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively execute all dependency patches.
|
||||
patch.dependencies?.forEach { dependencyClass ->
|
||||
val dependency = context.allPatches[dependencyClass]!!
|
||||
val result = executePatch(dependency, executedPatches)
|
||||
// Execute the patch.
|
||||
return try {
|
||||
execute(context)
|
||||
|
||||
result.exception?.let {
|
||||
return PatchResult(
|
||||
patch,
|
||||
PatchException(
|
||||
"'$patchName' depends on '${dependency.name ?: dependency}' " +
|
||||
"that raised an exception:\n${it.stackTraceToString()}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
PatchResult(this)
|
||||
} catch (exception: PatchException) {
|
||||
PatchResult(this, exception)
|
||||
} catch (exception: Exception) {
|
||||
PatchResult(this, PatchException(exception))
|
||||
}.also { executedPatches[this] = it }
|
||||
}
|
||||
|
||||
return try {
|
||||
patch.execute(context)
|
||||
// Prevent from decoding the app manifest twice if it is not needed.
|
||||
if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
|
||||
context.resourceContext.decodeResources(config.resourceMode)
|
||||
}
|
||||
|
||||
PatchResult(patch)
|
||||
logger.info("Executing patches")
|
||||
|
||||
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>()
|
||||
|
||||
context.executablePatches.sortedBy { it.name }.forEach { patch ->
|
||||
val patchResult = patch.execute(executedPatches)
|
||||
|
||||
// If an exception occurred or the patch has no finalize block, emit the result.
|
||||
if (patchResult.exception != null || patch.finalizeBlock == null) {
|
||||
emit(patchResult)
|
||||
}
|
||||
}
|
||||
|
||||
val succeededPatchesWithFinalizeBlock = executedPatches.values.filter {
|
||||
it.exception == null && it.patch.finalizeBlock != null
|
||||
}
|
||||
|
||||
succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult ->
|
||||
val patch = executionResult.patch
|
||||
|
||||
val result =
|
||||
try {
|
||||
patch.finalize(context)
|
||||
|
||||
executionResult
|
||||
} catch (exception: PatchException) {
|
||||
PatchResult(patch, exception)
|
||||
} catch (exception: Exception) {
|
||||
PatchResult(patch, PatchException(exception))
|
||||
}.also { executedPatches[patch] = it }
|
||||
}
|
||||
|
||||
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
|
||||
|
||||
LookupMap.initializeLookupMaps(context.bytecodeContext)
|
||||
|
||||
// Prevent from decoding the app manifest twice if it is not needed.
|
||||
if (config.resourceMode != ResourceContext.ResourceMode.NONE) {
|
||||
context.resourceContext.decodeResources(config.resourceMode)
|
||||
}
|
||||
|
||||
logger.info("Executing patches")
|
||||
|
||||
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
|
||||
|
||||
context.executablePatches.values.sortedBy { it.name }.forEach { patch ->
|
||||
val patchResult = executePatch(patch, executedPatches)
|
||||
|
||||
// If the patch failed, emit the result, even if it is closeable.
|
||||
// Results of executed patches that are closeable will be emitted later.
|
||||
patchResult.exception?.let {
|
||||
// Propagate exception to caller instead of wrapping it in a new exception.
|
||||
emit(patchResult)
|
||||
|
||||
if (returnOnError) return@flow
|
||||
} ?: run {
|
||||
if (patch is Closeable) return@run
|
||||
|
||||
emit(patchResult)
|
||||
}
|
||||
|
||||
if (result.exception != null) {
|
||||
emit(
|
||||
PatchResult(
|
||||
patch,
|
||||
PatchException(
|
||||
"The patch \"$patch\" raised an exception: ${result.exception.stackTraceToString()}",
|
||||
result.exception,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else if (patch in context.executablePatches) {
|
||||
emit(result)
|
||||
}
|
||||
|
||||
executedPatches.values
|
||||
.filter { it.exception == null }
|
||||
.filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
|
||||
val patch = executedPatch.patch
|
||||
|
||||
val result =
|
||||
try {
|
||||
(patch as Closeable).close()
|
||||
|
||||
executedPatch
|
||||
} catch (exception: PatchException) {
|
||||
PatchResult(patch, exception)
|
||||
} catch (exception: Exception) {
|
||||
PatchResult(patch, PatchException(exception))
|
||||
}
|
||||
|
||||
result.exception?.let {
|
||||
emit(
|
||||
PatchResult(
|
||||
patch,
|
||||
PatchException(
|
||||
"'$patch' raised an exception while being closed: ${it.stackTraceToString()}",
|
||||
result.exception,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (returnOnError) return@flow
|
||||
} ?: run {
|
||||
patch.name ?: return@run
|
||||
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() = LookupMap.clearLookupMaps()
|
||||
override fun close() = context.bytecodeContext.lookupMaps.close()
|
||||
|
||||
/**
|
||||
* Compile and save the patched APK file.
|
||||
* Compile and save patched APK files.
|
||||
*
|
||||
* @return The [PatcherResult] containing the patched input files.
|
||||
* @return The [PatcherResult] containing the patched APK files.
|
||||
*/
|
||||
@OptIn(InternalApi::class)
|
||||
override fun get() =
|
||||
PatcherResult(
|
||||
context.bytecodeContext.get(),
|
||||
context.resourceContext.get(),
|
||||
)
|
||||
fun get() = PatcherResult(context.bytecodeContext.get(), context.resourceContext.get())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.ResourcePatchContext
|
||||
import brut.androlib.Config
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
@@ -27,9 +27,9 @@ class PatcherConfig(
|
||||
/**
|
||||
* The mode to use for resource decoding and compiling.
|
||||
*
|
||||
* @see ResourceContext.ResourceMode
|
||||
* @see ResourcePatchContext.ResourceMode
|
||||
*/
|
||||
internal var resourceMode = ResourceContext.ResourceMode.NONE
|
||||
internal var resourceMode = ResourcePatchContext.ResourceMode.NONE
|
||||
|
||||
/**
|
||||
* The configuration for decoding and compiling resources.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.ResourcePatchContext
|
||||
import brut.androlib.apk.ApkInfo
|
||||
import brut.directory.ExtFile
|
||||
|
||||
@@ -19,22 +19,22 @@ class PatcherContext internal constructor(config: PatcherConfig) {
|
||||
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(config.apkFile)))
|
||||
|
||||
/**
|
||||
* The map of [Patch]es associated by their [PatchClass].
|
||||
* The set of [Patch]es.
|
||||
*/
|
||||
internal val executablePatches = mutableMapOf<PatchClass, Patch<*>>()
|
||||
internal val executablePatches = mutableSetOf<Patch<*>>()
|
||||
|
||||
/**
|
||||
* The map of all [Patch]es and their dependencies associated by their [PatchClass].
|
||||
* The set of all [Patch]es and their dependencies.
|
||||
*/
|
||||
internal val allPatches = mutableMapOf<PatchClass, Patch<*>>()
|
||||
internal val allPatches = mutableSetOf<Patch<*>>()
|
||||
|
||||
/**
|
||||
* A context for the patcher containing the current state of the resources.
|
||||
* The context for patches containing the current state of the resources.
|
||||
*/
|
||||
internal val resourceContext = ResourceContext(packageMetadata, config)
|
||||
internal val resourceContext = ResourcePatchContext(packageMetadata, config)
|
||||
|
||||
/**
|
||||
* A context for the patcher containing the current state of the bytecode.
|
||||
* The context for patches containing the current state of the bytecode.
|
||||
*/
|
||||
internal val bytecodeContext = BytecodeContext(config)
|
||||
internal val bytecodeContext = BytecodePatchContext(config)
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
/**
|
||||
* An exception thrown by ReVanced [Patcher].
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
* @param cause The corresponding [Throwable].
|
||||
*/
|
||||
sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
|
||||
constructor(errorMessage: String) : this(errorMessage, null)
|
||||
|
||||
class CircularDependencyException internal constructor(dependant: String) : PatcherException(
|
||||
"Patch '$dependant' causes a circular dependency",
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import java.io.File
|
||||
|
||||
@Deprecated("Use PatcherConfig instead.")
|
||||
data class PatcherOptions(
|
||||
internal val inputFile: File,
|
||||
internal val resourceCachePath: File = File("revanced-resource-cache"),
|
||||
internal val aaptBinaryPath: String? = null,
|
||||
internal val frameworkFileDirectory: String? = null,
|
||||
internal val multithreadingDexFileWriter: Boolean = false,
|
||||
) {
|
||||
@Deprecated("This method will be removed in the future.")
|
||||
fun recreateResourceCacheDirectory(): File {
|
||||
PatcherConfig(
|
||||
inputFile,
|
||||
resourceCachePath,
|
||||
aaptBinaryPath,
|
||||
frameworkFileDirectory,
|
||||
multithreadingDexFileWriter,
|
||||
).initializeTemporaryFilesDirectories()
|
||||
|
||||
return resourceCachePath
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package app.revanced.patcher
|
||||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
/**
|
||||
* The result of a patcher.
|
||||
@@ -15,87 +14,6 @@ class PatcherResult internal constructor(
|
||||
val dexFiles: Set<PatchedDexFile>,
|
||||
val resources: PatchedResources?,
|
||||
) {
|
||||
@Deprecated("This method is not used anymore")
|
||||
constructor(
|
||||
dexFiles: List<PatchedDexFile>,
|
||||
resourceFile: File?,
|
||||
doNotCompress: List<String>? = null,
|
||||
) : this(dexFiles.toSet(), PatchedResources(resourceFile, null, doNotCompress?.toSet() ?: emptySet(), emptySet()))
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun component1(): List<PatchedDexFile> {
|
||||
return dexFiles.toList()
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun component2(): File? {
|
||||
return resources?.resourcesApk
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun component3(): List<String>? {
|
||||
return resources?.doNotCompress?.toList()
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun copy(
|
||||
dexFiles: List<PatchedDexFile>,
|
||||
resourceFile: File?,
|
||||
doNotCompress: List<String>? = null,
|
||||
): PatcherResult {
|
||||
return PatcherResult(
|
||||
dexFiles.toSet(),
|
||||
PatchedResources(
|
||||
resourceFile,
|
||||
null,
|
||||
doNotCompress?.toSet() ?: emptySet(),
|
||||
emptySet(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
override fun toString(): String {
|
||||
return (("PatcherResult(dexFiles=" + this.dexFiles + ", resourceFile=" + this.resources?.resourcesApk) + ", doNotCompress=" + this.resources?.doNotCompress) + ")"
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
override fun hashCode(): Int {
|
||||
val result = dexFiles.hashCode()
|
||||
return (
|
||||
(
|
||||
(result * 31) +
|
||||
(if (this.resources?.resourcesApk == null) 0 else this.resources.resourcesApk.hashCode())
|
||||
) * 31
|
||||
) +
|
||||
(if (this.resources?.doNotCompress == null) 0 else this.resources.doNotCompress.hashCode())
|
||||
}
|
||||
|
||||
@Deprecated("This method is not used anymore")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (other is PatcherResult) {
|
||||
return Intrinsics.areEqual(this.dexFiles, other.dexFiles) && Intrinsics.areEqual(
|
||||
this.resources?.resourcesApk,
|
||||
other.resources?.resourcesApk,
|
||||
) && Intrinsics.areEqual(this.resources?.doNotCompress, other.resources?.doNotCompress)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun getDexFiles() = component1()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun getResourceFile() = component2()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("This method is not used anymore")
|
||||
fun getDoNotCompress() = component3()
|
||||
|
||||
/**
|
||||
* A dex file.
|
||||
@@ -103,10 +21,7 @@ class PatcherResult internal constructor(
|
||||
* @param name The original name of the dex file.
|
||||
* @param stream The dex file as [InputStream].
|
||||
*/
|
||||
class PatchedDexFile
|
||||
// TODO: Add internal modifier.
|
||||
@Deprecated("This constructor will be removed in the future.")
|
||||
constructor(val name: String, val stream: InputStream)
|
||||
class PatchedDexFile internal constructor(val name: String, val stream: InputStream)
|
||||
|
||||
/**
|
||||
* The resources of a patched apk.
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
@FunctionalInterface
|
||||
interface PatchesConsumer {
|
||||
@Deprecated("Use acceptPatches(PatchSet) instead.", ReplaceWith("acceptPatches(patches.toSet())"))
|
||||
fun acceptPatches(patches: List<Patch<*>>) = acceptPatches(patches.toSet())
|
||||
fun acceptPatches(patches: PatchSet)
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.InternalApi
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
import app.revanced.patcher.util.ProxyClassList
|
||||
import app.revanced.patcher.util.method.MethodWalker
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import com.android.tools.smali.dexlib2.Opcodes
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.DexFile
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.DexIO
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.Flushable
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for the patcher containing the current state of the bytecode.
|
||||
*
|
||||
* @param config The [PatcherConfig] used to create this context.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class BytecodeContext internal constructor(private val config: PatcherConfig) :
|
||||
Context<Set<PatcherResult.PatchedDexFile>> {
|
||||
private val logger = Logger.getLogger(BytecodeContext::class.java.name)
|
||||
|
||||
/**
|
||||
* [Opcodes] of the supplied [PatcherConfig.apkFile].
|
||||
*/
|
||||
internal lateinit var opcodes: Opcodes
|
||||
|
||||
/**
|
||||
* The list of classes.
|
||||
*/
|
||||
val classes by lazy {
|
||||
ProxyClassList(
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
config.apkFile,
|
||||
BasicDexFileNamer(),
|
||||
null,
|
||||
null,
|
||||
).also { opcodes = it.opcodes }.classes.toMutableSet(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [Integrations] of this [PatcherContext].
|
||||
*/
|
||||
internal val integrations = Integrations()
|
||||
|
||||
/**
|
||||
* Find a class by a given class name.
|
||||
*
|
||||
* @param className The name of the class.
|
||||
* @return A proxy for the first class that matches the class name.
|
||||
*/
|
||||
fun findClass(className: String) = findClass { it.type.contains(className) }
|
||||
|
||||
/**
|
||||
* Find a class by a given predicate.
|
||||
*
|
||||
* @param predicate A predicate to match the class.
|
||||
* @return A proxy for the first class that matches the predicate.
|
||||
*/
|
||||
fun findClass(predicate: (ClassDef) -> Boolean) =
|
||||
// if we already proxied the class matching the predicate...
|
||||
classes.proxies.firstOrNull { predicate(it.immutableClass) }
|
||||
?: // else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||
classes.find(predicate)?.let { proxy(it) }
|
||||
|
||||
/**
|
||||
* Proxy a class.
|
||||
* This will allow the class to be modified.
|
||||
*
|
||||
* @param classDef The class to proxy.
|
||||
* @return A proxy for the class.
|
||||
*/
|
||||
fun proxy(classDef: ClassDef) =
|
||||
this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
|
||||
ClassProxy(classDef).also { this.classes.add(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||
*
|
||||
* @param startMethod The method to start at.
|
||||
* @return A [MethodWalker] instance.
|
||||
*/
|
||||
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
|
||||
|
||||
/**
|
||||
* Compile bytecode from the [BytecodeContext].
|
||||
*
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
@InternalApi
|
||||
override fun get(): Set<PatcherResult.PatchedDexFile> {
|
||||
logger.info("Compiling patched dex files")
|
||||
|
||||
val patchedDexFileResults =
|
||||
config.patchedFiles.resolve("dex").also {
|
||||
it.deleteRecursively() // Make sure the directory is empty.
|
||||
it.mkdirs()
|
||||
}.apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true,
|
||||
if (config.multithreadingDexFileWriter) -1 else 1,
|
||||
this,
|
||||
BasicDexFileNamer(),
|
||||
object : DexFile {
|
||||
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
|
||||
|
||||
override fun getOpcodes() = this@BytecodeContext.opcodes
|
||||
},
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
||||
) { _, entryName, _ -> logger.info("Compiled $entryName") }
|
||||
}.listFiles(FileFilter { it.isFile })!!.map {
|
||||
@Suppress("DEPRECATION")
|
||||
PatcherResult.PatchedDexFile(it.name, it.inputStream())
|
||||
}.toSet()
|
||||
|
||||
System.gc()
|
||||
|
||||
return patchedDexFileResults
|
||||
}
|
||||
|
||||
/**
|
||||
* The integrations of a [PatcherContext].
|
||||
*/
|
||||
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
|
||||
/**
|
||||
* Whether to merge integrations.
|
||||
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
|
||||
*/
|
||||
var merge = false
|
||||
|
||||
/**
|
||||
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
|
||||
*/
|
||||
override fun flush() {
|
||||
if (!merge) return
|
||||
|
||||
logger.info("Merging integrations")
|
||||
|
||||
val classMap = classes.associateBy { it.type }
|
||||
|
||||
this@Integrations.forEach { integrations ->
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
integrations,
|
||||
BasicDexFileNamer(),
|
||||
null,
|
||||
null,
|
||||
).classes.forEach classDef@{ classDef ->
|
||||
val existingClass =
|
||||
classMap[classDef.type] ?: run {
|
||||
logger.fine("Adding $classDef")
|
||||
classes.add(classDef)
|
||||
return@classDef
|
||||
}
|
||||
|
||||
logger.fine("$classDef exists. Adding missing methods and fields.")
|
||||
|
||||
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
|
||||
// If the class was merged, replace the original class with the merged class.
|
||||
if (mergedClass === existingClass) return@let
|
||||
classes.apply {
|
||||
remove(existingClass)
|
||||
add(mergedClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package app.revanced.patcher.data
|
||||
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* A common interface for contexts such as [ResourceContext] and [BytecodeContext].
|
||||
*/
|
||||
|
||||
sealed interface Context<T> : Supplier<T>
|
||||
@@ -1,61 +0,0 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
internal object AnnotationExtensions {
|
||||
/**
|
||||
* Search for an annotation recursively.
|
||||
*
|
||||
* @param targetAnnotationClass The annotation class to search for.
|
||||
* @param searchedClasses A set of annotations that have already been searched.
|
||||
* @return The annotation if found, otherwise null.
|
||||
*/
|
||||
fun <T : Annotation> Class<*>.findAnnotationRecursively(
|
||||
targetAnnotationClass: Class<T>,
|
||||
searchedClasses: HashSet<Annotation> = hashSetOf(),
|
||||
): T? {
|
||||
annotations.forEach { annotation ->
|
||||
// Terminate if the annotation is already searched.
|
||||
if (annotation in searchedClasses) return@forEach
|
||||
searchedClasses.add(annotation)
|
||||
|
||||
// Terminate if the annotation is found.
|
||||
if (targetAnnotationClass == annotation.annotationClass.java) return annotation as T
|
||||
|
||||
return annotation.annotationClass.java.findAnnotationRecursively(
|
||||
targetAnnotationClass,
|
||||
searchedClasses,
|
||||
) ?: return@forEach
|
||||
}
|
||||
|
||||
// Search the super class.
|
||||
superclass?.findAnnotationRecursively(
|
||||
targetAnnotationClass,
|
||||
searchedClasses,
|
||||
)?.let { return it }
|
||||
|
||||
// Search the interfaces.
|
||||
interfaces.forEach { superClass ->
|
||||
return superClass.findAnnotationRecursively(
|
||||
targetAnnotationClass,
|
||||
searchedClasses,
|
||||
) ?: return@forEach
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an annotation recursively.
|
||||
*
|
||||
* First the annotations, then the annotated classes super class and then it's interfaces
|
||||
* are searched for the annotation recursively.
|
||||
*
|
||||
* @param targetAnnotation The annotation to search for.
|
||||
* @return The annotation if found, otherwise null.
|
||||
*/
|
||||
fun <T : Annotation> KClass<*>.findAnnotationRecursively(targetAnnotation: KClass<T>) =
|
||||
java.findAnnotationRecursively(targetAnnotation.java)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
/**
|
||||
* Create a label for the instruction at given index.
|
||||
@@ -10,24 +9,3 @@ import com.android.tools.smali.dexlib2.AccessFlags
|
||||
* @return The label.
|
||||
*/
|
||||
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
|
||||
|
||||
/**
|
||||
* Perform a bitwise OR operation between an [AccessFlags] and an [Int].
|
||||
*
|
||||
* @param other The [Int] to perform the operation with.
|
||||
*/
|
||||
infix fun Int.or(other: AccessFlags) = this or other.value
|
||||
|
||||
/**
|
||||
* Perform a bitwise OR operation between two [AccessFlags].
|
||||
*
|
||||
* @param other The other [AccessFlags] to perform the operation with.
|
||||
*/
|
||||
infix fun AccessFlags.or(other: AccessFlags) = value or other.value
|
||||
|
||||
/**
|
||||
* Perform a bitwise OR operation between an [Int] and an [AccessFlags].
|
||||
*
|
||||
* @param other The [AccessFlags] to perform the operation with.
|
||||
*/
|
||||
infix fun AccessFlags.or(other: Int) = value or other
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||
import com.android.tools.smali.dexlib2.builder.Label
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.builder.instruction.*
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.MethodImplementation
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
|
||||
object InstructionExtensions {
|
||||
@@ -30,7 +32,7 @@ object InstructionExtensions {
|
||||
* @param instructions The instructions to add.
|
||||
*/
|
||||
fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) =
|
||||
instructions.forEach { this.addInstruction(it) }
|
||||
instructions.forEach { addInstruction(it) }
|
||||
|
||||
/**
|
||||
* Remove instructions from a method at the given index.
|
||||
@@ -178,8 +180,8 @@ object InstructionExtensions {
|
||||
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
|
||||
|
||||
/**
|
||||
* Creates a new label for the instruction
|
||||
* and replaces it with the label of the [compiledInstruction] at [compiledInstructionIndex].
|
||||
* Create a new label for the instruction
|
||||
* and replace it with the label of the [compiledInstruction] at [compiledInstructionIndex].
|
||||
*/
|
||||
fun Instruction.makeNewLabel() {
|
||||
fun replaceOffset(
|
||||
@@ -310,6 +312,24 @@ object InstructionExtensions {
|
||||
smaliInstructions: String,
|
||||
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
*
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun MethodImplementation.getInstruction(index: Int) = instructions.elementAt(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
*
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> MethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
*
|
||||
@@ -328,12 +348,27 @@ object InstructionExtensions {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> MutableMethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun Method.getInstructionOrNull(index: Int): Instruction? = implementation?.getInstruction(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = implementation!!.getInstruction(index)
|
||||
fun Method.getInstruction(index: Int): Instruction = getInstructionOrNull(index)!!
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun <T> Method.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
@@ -341,11 +376,59 @@ object InstructionExtensions {
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun <T> MutableMethod.getInstruction(index: Int): T = implementation!!.getInstruction<T>(index)
|
||||
fun <T> Method.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
|
||||
|
||||
/**
|
||||
* Get the instructions of a method.
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun MutableMethod.getInstructionOrNull(index: Int): BuilderInstruction? = implementation?.getInstruction(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = getInstructionOrNull(index)!!
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun <T> MutableMethod.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun <T> MutableMethod.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions or null if the method has no implementation.
|
||||
*/
|
||||
val Method.instructionsOrNull: Iterable<Instruction>? get() = implementation?.instructions
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions.
|
||||
*/
|
||||
fun MutableMethod.getInstructions(): MutableList<BuilderInstruction> = implementation!!.instructions
|
||||
val Method.instructions: Iterable<Instruction> get() = instructionsOrNull!!
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions or null if the method has no implementation.
|
||||
*/
|
||||
val MutableMethod.instructionsOrNull: MutableList<BuilderInstruction>? get() = implementation?.instructions
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions.
|
||||
*/
|
||||
val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instructionsOrNull!!
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
|
||||
|
||||
object MethodFingerprintExtensions {
|
||||
/**
|
||||
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
|
||||
*/
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
@Deprecated(
|
||||
message = "Use the property instead.",
|
||||
replaceWith = ReplaceWith("this.fuzzyPatternScanMethod"),
|
||||
)
|
||||
val MethodFingerprint.fuzzyPatternScanMethod
|
||||
get() = this.fuzzyPatternScanMethod
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package app.revanced.patcher.fingerprint
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
import java.util.*
|
||||
|
||||
internal typealias MethodClassPair = Pair<Method, ClassDef>
|
||||
|
||||
/**
|
||||
* Lookup map for methods.
|
||||
*/
|
||||
internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by mutableMapOf() {
|
||||
/**
|
||||
* Adds a [MethodClassPair] to the list associated with the given key.
|
||||
* If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
|
||||
*/
|
||||
fun add(
|
||||
key: String,
|
||||
methodClassPair: MethodClassPair,
|
||||
) {
|
||||
getOrPut(key) { MethodClassList() }.add(methodClassPair)
|
||||
}
|
||||
|
||||
/**
|
||||
* List of methods and the class they are a member of.
|
||||
*/
|
||||
internal class MethodClassList : LinkedList<MethodClassPair>()
|
||||
|
||||
companion object Maps {
|
||||
/**
|
||||
* A list of methods and the class they are a member of.
|
||||
*/
|
||||
internal val methods = MethodClassList()
|
||||
|
||||
/**
|
||||
* Lookup map for methods keyed to the methods access flags, return type and parameter.
|
||||
*/
|
||||
internal val methodSignatureLookupMap = LookupMap()
|
||||
|
||||
/**
|
||||
* Lookup map for methods associated by strings referenced in the method.
|
||||
*/
|
||||
internal val methodStringsLookupMap = LookupMap()
|
||||
|
||||
/**
|
||||
* 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 initializeLookupMaps(context: BytecodeContext) {
|
||||
if (methods.isNotEmpty()) clearLookupMaps()
|
||||
|
||||
context.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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the internal lookup maps created in [initializeLookupMaps].
|
||||
*/
|
||||
internal fun clearLookupMaps() {
|
||||
methods.clear()
|
||||
methodSignatureLookupMap.clear()
|
||||
methodStringsLookupMap.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a string based on the parameter reference types of this method.
|
||||
*/
|
||||
internal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
package app.revanced.patcher.fingerprint
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.fingerprint.LookupMap.Maps.appendParameters
|
||||
import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps
|
||||
import app.revanced.patcher.fingerprint.LookupMap.Maps.methodSignatureLookupMap
|
||||
import app.revanced.patcher.fingerprint.LookupMap.Maps.methodStringsLookupMap
|
||||
import app.revanced.patcher.fingerprint.LookupMap.Maps.methods
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult
|
||||
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class MethodFingerprint(
|
||||
internal val returnType: String? = null,
|
||||
internal val accessFlags: Int? = null,
|
||||
internal val parameters: Iterable<String>? = null,
|
||||
internal val opcodes: Iterable<Opcode?>? = null,
|
||||
internal val strings: Iterable<String>? = null,
|
||||
internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null,
|
||||
) {
|
||||
/**
|
||||
* The result of the [MethodFingerprint].
|
||||
*/
|
||||
var result: MethodFingerprintResult? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The [FuzzyPatternScanMethod] annotation of the [MethodFingerprint].
|
||||
*
|
||||
* If the annotation is not present, this property is null.
|
||||
*/
|
||||
val fuzzyPatternScanMethod = this::class.findAnnotationRecursively(FuzzyPatternScanMethod::class)
|
||||
|
||||
/**
|
||||
* Resolve a [MethodFingerprint] using the lookup map built by [initializeLookupMaps].
|
||||
*
|
||||
* [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 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(): LookupMap.MethodClassList? {
|
||||
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(): LookupMap.MethodClassList {
|
||||
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] ?: return LookupMap.MethodClassList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a [MethodFingerprint] using a list of [MethodClassPair].
|
||||
*
|
||||
* @return True if the resolution was successful, false otherwise.
|
||||
*/
|
||||
fun MethodFingerprint.resolveUsingMethodClassPair(methodClasses: LookupMap.MethodClassList): Boolean {
|
||||
methodClasses.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 [MethodFingerprint] against a [ClassDef].
|
||||
*
|
||||
* @param forClass The class on which to resolve the [MethodFingerprint] in.
|
||||
* @param context The [BytecodeContext] to host proxies.
|
||||
* @return True if the resolution was successful, false otherwise.
|
||||
*/
|
||||
fun resolve(
|
||||
context: BytecodeContext,
|
||||
forClass: ClassDef,
|
||||
): Boolean {
|
||||
for (method in forClass.methods)
|
||||
if (resolve(context, method, forClass)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a [MethodFingerprint] against a [Method].
|
||||
*
|
||||
* @param method The class on which to resolve the [MethodFingerprint] in.
|
||||
* @param forClass The class on which to resolve the [MethodFingerprint].
|
||||
* @param context The [BytecodeContext] to host proxies.
|
||||
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
|
||||
*/
|
||||
fun resolve(
|
||||
context: BytecodeContext,
|
||||
method: Method,
|
||||
forClass: ClassDef,
|
||||
): Boolean {
|
||||
val methodFingerprint = this
|
||||
|
||||
if (methodFingerprint.result != null) return true
|
||||
|
||||
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) {
|
||||
return false
|
||||
}
|
||||
|
||||
fun parametersEqual(
|
||||
parameters1: Iterable<CharSequence>,
|
||||
parameters2: Iterable<CharSequence>,
|
||||
): Boolean {
|
||||
if (parameters1.count() != parameters2.count()) return false
|
||||
val iterator1 = parameters1.iterator()
|
||||
parameters2.forEach {
|
||||
if (!it.startsWith(iterator1.next())) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (methodFingerprint.parameters != null &&
|
||||
!parametersEqual(
|
||||
methodFingerprint.parameters, // TODO: parseParameters()
|
||||
method.parameterTypes,
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass)) {
|
||||
return false
|
||||
}
|
||||
|
||||
val stringsScanResult: StringsScanResult? =
|
||||
if (methodFingerprint.strings != null) {
|
||||
StringsScanResult(
|
||||
buildList {
|
||||
val implementation = method.implementation ?: return false
|
||||
|
||||
val stringsList = methodFingerprint.strings.toMutableList()
|
||||
|
||||
implementation.instructions.forEachIndexed { instructionIndex, instruction ->
|
||||
if (
|
||||
instruction.opcode != Opcode.CONST_STRING &&
|
||||
instruction.opcode != Opcode.CONST_STRING_JUMBO
|
||||
) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
|
||||
val index = stringsList.indexOfFirst(string::contains)
|
||||
if (index == -1) return@forEachIndexed
|
||||
|
||||
add(StringsScanResult.StringMatch(string, instructionIndex))
|
||||
stringsList.removeAt(index)
|
||||
}
|
||||
|
||||
if (stringsList.isNotEmpty()) return false
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val patternScanResult =
|
||||
if (methodFingerprint.opcodes != null) {
|
||||
method.implementation?.instructions ?: return false
|
||||
|
||||
fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings(
|
||||
pattern: Iterable<Opcode?>,
|
||||
instructions: Iterable<Instruction>,
|
||||
) = buildList {
|
||||
for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) {
|
||||
val originalOpcode = instructions.elementAt(instructionIndex).opcode
|
||||
val patternOpcode = pattern.elementAt(patternIndex)
|
||||
|
||||
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
|
||||
|
||||
this.add(
|
||||
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
|
||||
originalOpcode,
|
||||
patternOpcode,
|
||||
instructionIndex,
|
||||
patternIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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.newWarnings(pattern, instructions)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
method.patternScan(methodFingerprint) ?: return false
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
methodFingerprint.result =
|
||||
MethodFingerprintResult(
|
||||
method,
|
||||
forClass,
|
||||
MethodFingerprintResult.MethodFingerprintScanResult(
|
||||
patternScanResult,
|
||||
stringsScanResult,
|
||||
),
|
||||
context,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Resolve a list of [MethodFingerprint] using the lookup map built by [initializeLookupMaps].
|
||||
*
|
||||
* [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 Set<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
|
||||
if (methods.isEmpty()) throw PatchException("lookup map not initialized")
|
||||
|
||||
forEach { fingerprint ->
|
||||
fingerprint.resolveUsingLookupMap(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
|
||||
*
|
||||
* @param classes The classes on which to resolve the [MethodFingerprint] in.
|
||||
* @param context The [BytecodeContext] to host proxies.
|
||||
* @return True if the resolution was successful, false otherwise.
|
||||
*/
|
||||
fun Iterable<MethodFingerprint>.resolve(
|
||||
context: BytecodeContext,
|
||||
classes: Iterable<ClassDef>,
|
||||
) = forEach { fingerprint ->
|
||||
for (classDef in classes) {
|
||||
if (fingerprint.resolve(context, classDef)) break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package app.revanced.patcher.fingerprint
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
/**
|
||||
* Represents the result of a [MethodFingerprintResult].
|
||||
*
|
||||
* @param method The matching method.
|
||||
* @param classDef The [ClassDef] that contains the matching [method].
|
||||
* @param scanResult The result of scanning for the [MethodFingerprint].
|
||||
* @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class MethodFingerprintResult(
|
||||
val method: Method,
|
||||
val classDef: ClassDef,
|
||||
val scanResult: MethodFingerprintScanResult,
|
||||
internal val context: BytecodeContext,
|
||||
) {
|
||||
/**
|
||||
* Returns a mutable clone of [classDef]
|
||||
*
|
||||
* Please note, this method allocates a [ClassProxy].
|
||||
* Use [classDef] where possible.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
||||
|
||||
/**
|
||||
* Returns a mutable clone of [method]
|
||||
*
|
||||
* Please note, this method allocates a [ClassProxy].
|
||||
* Use [method] where possible.
|
||||
*/
|
||||
val mutableMethod by lazy {
|
||||
mutableClass.methods.first {
|
||||
MethodUtil.methodSignaturesMatch(it, this.method)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of scanning on the [MethodFingerprint].
|
||||
* @param patternScanResult The result of the pattern scan.
|
||||
* @param stringsScanResult The result of the string scan.
|
||||
*/
|
||||
class MethodFingerprintScanResult(
|
||||
val patternScanResult: PatternScanResult?,
|
||||
val stringsScanResult: StringsScanResult?,
|
||||
) {
|
||||
/**
|
||||
* The result of scanning strings on the [MethodFingerprint].
|
||||
* @param matches The list of strings that were matched.
|
||||
*/
|
||||
class StringsScanResult(val matches: List<StringMatch>) {
|
||||
/**
|
||||
* Represents a match for a string at an index.
|
||||
* @param string The string that was matched.
|
||||
* @param index The index of the string.
|
||||
*/
|
||||
class StringMatch(val string: String, val index: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a pattern scan.
|
||||
* @param startIndex The start index of the instructions where to which this pattern matches.
|
||||
* @param endIndex The end index of the instructions where to which this pattern matches.
|
||||
* @param warnings A list of warnings considering this [PatternScanResult].
|
||||
*/
|
||||
class PatternScanResult(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int,
|
||||
var warnings: List<Warning>? = null,
|
||||
) {
|
||||
/**
|
||||
* Represents warnings of the pattern scan.
|
||||
* @param correctOpcode The opcode the instruction list has.
|
||||
* @param wrongOpcode The opcode the pattern list of the signature currently has.
|
||||
* @param instructionIndex The index of the opcode relative to the instruction list.
|
||||
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
|
||||
*/
|
||||
class Warning(
|
||||
val correctOpcode: Opcode,
|
||||
val wrongOpcode: Opcode,
|
||||
val instructionIndex: Int,
|
||||
val patternIndex: Int,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package app.revanced.patcher.fingerprint.annotation
|
||||
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||
|
||||
/**
|
||||
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
|
||||
* @param threshold if [threshold] or more of the opcodes do not match, skip.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class FuzzyPatternScanMethod(
|
||||
val threshold: Int = 1,
|
||||
)
|
||||
@@ -1,68 +0,0 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.PatchClass
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprint.Companion.resolveUsingLookupMap
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* A [Patch] that accesses a [BytecodeContext].
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
*/
|
||||
@Suppress("unused")
|
||||
abstract class BytecodePatch : Patch<BytecodeContext> {
|
||||
/**
|
||||
* The fingerprints to resolve before executing the patch.
|
||||
*/
|
||||
internal val fingerprints: Set<MethodFingerprint>
|
||||
|
||||
/**
|
||||
* Create a new [BytecodePatch].
|
||||
*
|
||||
* @param fingerprints The fingerprints to resolve before executing the patch.
|
||||
*/
|
||||
constructor(fingerprints: Set<MethodFingerprint> = emptySet()) {
|
||||
this.fingerprints = fingerprints
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new [BytecodePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* @param description The description of the patch.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||
*/
|
||||
constructor(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
compatiblePackages: Set<CompatiblePackage>? = null,
|
||||
dependencies: Set<PatchClass>? = null,
|
||||
use: Boolean = true,
|
||||
requiresIntegrations: Boolean = false,
|
||||
fingerprints: Set<MethodFingerprint> = emptySet(),
|
||||
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations) {
|
||||
this.fingerprints = fingerprints
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new [BytecodePatch].
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the constructor with fingerprints instead.",
|
||||
ReplaceWith("BytecodePatch(emptySet())"),
|
||||
)
|
||||
constructor() : this(emptySet())
|
||||
|
||||
override fun execute(context: PatcherContext) {
|
||||
fingerprints.resolveUsingLookupMap(context.bytecodeContext)
|
||||
execute(context.bytecodeContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.InternalApi
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
import app.revanced.patcher.util.MethodNavigator
|
||||
import app.revanced.patcher.util.ProxyClassList
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.Opcodes
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.DexFile
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.DexIO
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import lanchon.multidexlib2.RawDexIO
|
||||
import java.io.Closeable
|
||||
import java.io.FileFilter
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for patches containing the current state of the bytecode.
|
||||
*
|
||||
* @param config The [PatcherConfig] used to create this context.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class BytecodePatchContext internal constructor(private val config: PatcherConfig) : PatchContext<Set<PatcherResult.PatchedDexFile>> {
|
||||
private val logger = Logger.getLogger(BytecodePatchContext::class.java.name)
|
||||
|
||||
/**
|
||||
* [Opcodes] of the supplied [PatcherConfig.apkFile].
|
||||
*/
|
||||
internal val opcodes: Opcodes
|
||||
|
||||
/**
|
||||
* The list of classes.
|
||||
*/
|
||||
val classes = ProxyClassList(
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
config.apkFile,
|
||||
BasicDexFileNamer(),
|
||||
null,
|
||||
null,
|
||||
).also { opcodes = it.opcodes }.classes.toMutableList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* The lookup maps for methods and the class they are a member of from the [classes].
|
||||
*/
|
||||
internal val lookupMaps by lazy { LookupMaps(classes) }
|
||||
|
||||
/**
|
||||
* Merge an extension to [classes].
|
||||
*
|
||||
* @param extensionInputStream The input stream of the extension to merge.
|
||||
*/
|
||||
internal fun merge(extensionInputStream: InputStream) {
|
||||
val extension = extensionInputStream.readAllBytes()
|
||||
|
||||
RawDexIO.readRawDexFile(extension, 0, null).classes.forEach { classDef ->
|
||||
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
|
||||
logger.fine("Adding class \"$classDef\"")
|
||||
|
||||
lookupMaps.classesByType[classDef.type] = classDef
|
||||
classes += classDef
|
||||
|
||||
return@forEach
|
||||
}
|
||||
|
||||
logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.")
|
||||
|
||||
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
|
||||
// If the class was merged, replace the original class with the merged class.
|
||||
if (mergedClass === existingClass) {
|
||||
return@let
|
||||
}
|
||||
|
||||
classes -= existingClass
|
||||
classes += mergedClass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a class by its type using a contains check.
|
||||
*
|
||||
* @param type The type of the class.
|
||||
* @return A proxy for the first class that matches the type.
|
||||
*/
|
||||
fun classByType(type: String) = classBy { type in it.type }
|
||||
|
||||
/**
|
||||
* Find a class with a predicate.
|
||||
*
|
||||
* @param predicate A predicate to match the class.
|
||||
* @return A proxy for the first class that matches the predicate.
|
||||
*/
|
||||
fun classBy(predicate: (ClassDef) -> Boolean) =
|
||||
classes.proxyPool.find { predicate(it.immutableClass) } ?: classes.find(predicate)?.let { proxy(it) }
|
||||
|
||||
/**
|
||||
* Proxy the class to allow mutation.
|
||||
*
|
||||
* @param classDef The class to proxy.
|
||||
*
|
||||
* @return A proxy for the class.
|
||||
*/
|
||||
fun proxy(classDef: ClassDef) = this@BytecodePatchContext.classes.proxyPool.find {
|
||||
it.immutableClass.type == classDef.type
|
||||
} ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) }
|
||||
|
||||
/**
|
||||
* Navigate a method.
|
||||
*
|
||||
* @param method The method to navigate.
|
||||
*
|
||||
* @return A [MethodNavigator] for the method.
|
||||
*/
|
||||
fun navigate(method: Method) = MethodNavigator(this@BytecodePatchContext, method)
|
||||
|
||||
/**
|
||||
* Compile bytecode from the [BytecodePatchContext].
|
||||
*
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
@InternalApi
|
||||
override fun get(): Set<PatcherResult.PatchedDexFile> {
|
||||
logger.info("Compiling patched dex files")
|
||||
|
||||
val patchedDexFileResults =
|
||||
config.patchedFiles.resolve("dex").also {
|
||||
it.deleteRecursively() // Make sure the directory is empty.
|
||||
it.mkdirs()
|
||||
}.apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true,
|
||||
if (config.multithreadingDexFileWriter) -1 else 1,
|
||||
this,
|
||||
BasicDexFileNamer(),
|
||||
object : DexFile {
|
||||
override fun getClasses() =
|
||||
this@BytecodePatchContext.classes.also(ProxyClassList::replaceClasses).toSet()
|
||||
|
||||
override fun getOpcodes() = this@BytecodePatchContext.opcodes
|
||||
},
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
||||
) { _, entryName, _ -> logger.info("Compiled $entryName") }
|
||||
}.listFiles(FileFilter { it.isFile })!!.map {
|
||||
PatcherResult.PatchedDexFile(it.name, it.inputStream())
|
||||
}.toSet()
|
||||
|
||||
System.gc()
|
||||
|
||||
return patchedDexFileResults
|
||||
}
|
||||
|
||||
/**
|
||||
* A lookup map for methods and the class they are a member of and classes.
|
||||
*
|
||||
* @param classes The list of classes to create the lookup maps from.
|
||||
*/
|
||||
internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable {
|
||||
/**
|
||||
* Classes associated by their type.
|
||||
*/
|
||||
internal val classesByType = classes.associateBy { it.type }.toMutableMap()
|
||||
|
||||
/**
|
||||
* All methods and the class they are a member of.
|
||||
*/
|
||||
internal val allMethods = MethodClassPairs()
|
||||
|
||||
/**
|
||||
* Methods associated by its access flags, return type and parameter.
|
||||
*/
|
||||
internal val methodsBySignature = MethodClassPairsLookupMap()
|
||||
|
||||
/**
|
||||
* Methods associated by strings referenced in it.
|
||||
*/
|
||||
internal val methodsByStrings = MethodClassPairsLookupMap()
|
||||
|
||||
init {
|
||||
classes.forEach { classDef ->
|
||||
classDef.methods.forEach { method ->
|
||||
val methodClassPair: MethodClassPair = method to classDef
|
||||
|
||||
// For fingerprints with no access or return type specified.
|
||||
allMethods += methodClassPair
|
||||
|
||||
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
|
||||
|
||||
// Add <access><returnType> as the key.
|
||||
methodsBySignature[accessFlagsReturnKey] = methodClassPair
|
||||
|
||||
// Add <access><returnType>[parameters] as the key.
|
||||
methodsBySignature[
|
||||
buildString {
|
||||
append(accessFlagsReturnKey)
|
||||
appendParameters(method.parameterTypes)
|
||||
},
|
||||
] = methodClassPair
|
||||
|
||||
// Add strings contained in the method as the key.
|
||||
method.instructionsOrNull?.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
|
||||
|
||||
methodsByStrings[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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
/**
|
||||
* Appends a string based on the parameter reference types of this method.
|
||||
*/
|
||||
internal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
allMethods.clear()
|
||||
methodsBySignature.clear()
|
||||
methodsByStrings.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A pair of a [Method] and the [ClassDef] it is a member of.
|
||||
*/
|
||||
internal typealias MethodClassPair = Pair<Method, ClassDef>
|
||||
|
||||
/**
|
||||
* A list of [MethodClassPair]s.
|
||||
*/
|
||||
internal typealias MethodClassPairs = LinkedList<MethodClassPair>
|
||||
|
||||
/**
|
||||
* A lookup map for [MethodClassPairs]s.
|
||||
* The key is a string and the value is a list of [MethodClassPair]s.
|
||||
*/
|
||||
internal class MethodClassPairsLookupMap : MutableMap<String, MethodClassPairs> by mutableMapOf() {
|
||||
/**
|
||||
* Add a [MethodClassPair] associated by any key.
|
||||
* If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
|
||||
*/
|
||||
internal operator fun set(key: String, methodClassPair: MethodClassPair) =
|
||||
apply { getOrPut(key) { MethodClassPairs() }.add(methodClassPair) }
|
||||
}
|
||||
548
src/main/kotlin/app/revanced/patcher/patch/Option.kt
Normal file
548
src/main/kotlin/app/revanced/patcher/patch/Option.kt
Normal file
@@ -0,0 +1,548 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
* An option.
|
||||
*
|
||||
* @param T The value type of the option.
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param type The type of the option value (to handle type erasure).
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @constructor Create a new [Option].
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
class Option<T> @PublishedApi internal constructor(
|
||||
val key: String,
|
||||
val default: T? = null,
|
||||
val values: Map<String, T?>? = null,
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
val required: Boolean = false,
|
||||
val type: KType,
|
||||
val validator: Option<T>.(T?) -> Boolean = { true },
|
||||
) {
|
||||
/**
|
||||
* The value of the [Option].
|
||||
*/
|
||||
var value: T?
|
||||
/**
|
||||
* Set the value of the [Option].
|
||||
*
|
||||
* @param value The value to set.
|
||||
*
|
||||
* @throws OptionException.ValueRequiredException If the value is required but null.
|
||||
* @throws OptionException.ValueValidationException If the value is invalid.
|
||||
*/
|
||||
set(value) {
|
||||
assertRequiredButNotNull(value)
|
||||
assertValid(value)
|
||||
|
||||
uncheckedValue = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the [Option].
|
||||
*
|
||||
* @return The value.
|
||||
*
|
||||
* @throws OptionException.ValueRequiredException If the value is required but null.
|
||||
* @throws OptionException.ValueValidationException If the value is invalid.
|
||||
*/
|
||||
get() {
|
||||
assertRequiredButNotNull(uncheckedValue)
|
||||
assertValid(uncheckedValue)
|
||||
|
||||
return uncheckedValue
|
||||
}
|
||||
|
||||
// The unchecked value is used to allow setting the value without validation.
|
||||
private var uncheckedValue = default
|
||||
|
||||
/**
|
||||
* Reset the [Option] to its default value.
|
||||
* Override this method if you need to mutate the value instead of replacing it.
|
||||
*/
|
||||
fun reset() {
|
||||
uncheckedValue = default
|
||||
}
|
||||
|
||||
private fun assertRequiredButNotNull(value: T?) {
|
||||
if (required && value == null) throw OptionException.ValueRequiredException(this)
|
||||
}
|
||||
|
||||
private fun assertValid(value: T?) {
|
||||
if (!validator(value)) throw OptionException.ValueValidationException(value, this)
|
||||
}
|
||||
|
||||
override fun toString() = value.toString()
|
||||
|
||||
operator fun getValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
) = value
|
||||
|
||||
operator fun setValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
value: T?,
|
||||
) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of [Option]s where options can be set and retrieved by key.
|
||||
*
|
||||
* @param options The options.
|
||||
*
|
||||
* @constructor Create a new [Options].
|
||||
*/
|
||||
class Options internal constructor(
|
||||
private val options: Map<String, Option<*>>,
|
||||
) : Map<String, Option<*>> by options {
|
||||
internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.key })
|
||||
|
||||
/**
|
||||
* Set an option's value.
|
||||
*
|
||||
* @param key The key.
|
||||
* @param value The value.
|
||||
*
|
||||
* @throws OptionException.OptionNotFoundException If the option does not exist.
|
||||
*/
|
||||
operator fun <T : Any> set(key: String, value: T?) {
|
||||
val option = this[key]
|
||||
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(option as Option<T>).value = value
|
||||
} catch (e: ClassCastException) {
|
||||
throw OptionException.InvalidValueTypeException(
|
||||
value?.let { it::class.java.name } ?: "null",
|
||||
option.value?.let { it::class.java.name } ?: "null",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option.
|
||||
*
|
||||
* @param key The key.
|
||||
*
|
||||
* @return The option.
|
||||
*/
|
||||
override fun get(key: String) = options[key] ?: throw OptionException.OptionNotFoundException(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a string value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.stringOption(
|
||||
key: String,
|
||||
default: String? = null,
|
||||
values: Map<String, String?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<String>.(String?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with an integer value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.intOption(
|
||||
key: String,
|
||||
default: Int? = null,
|
||||
values: Map<String, Int?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Int?>.(Int?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a boolean value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.booleanOption(
|
||||
key: String,
|
||||
default: Boolean? = null,
|
||||
values: Map<String, Boolean?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Boolean?>.(Boolean?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a float value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.floatOption(
|
||||
key: String,
|
||||
default: Float? = null,
|
||||
values: Map<String, Float?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Float?>.(Float?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a long value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.longOption(
|
||||
key: String,
|
||||
default: Long? = null,
|
||||
values: Map<String, Long?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Long?>.(Long?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a string list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.stringsOption(
|
||||
key: String,
|
||||
default: List<String>? = null,
|
||||
values: Map<String, List<String>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<String>>.(List<String>?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with an integer list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.intsOption(
|
||||
key: String,
|
||||
default: List<Int>? = null,
|
||||
values: Map<String, List<Int>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Int>>.(List<Int>?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a boolean list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.booleansOption(
|
||||
key: String,
|
||||
default: List<Boolean>? = null,
|
||||
values: Map<String, List<Boolean>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Boolean>>.(List<Boolean>?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a float list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.floatsOption(
|
||||
key: String,
|
||||
default: List<Float>? = null,
|
||||
values: Map<String, List<Float>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Float>>.(List<Float>?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a long list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.longsOption(
|
||||
key: String,
|
||||
default: List<Long>? = null,
|
||||
values: Map<String, List<Long>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Long>>.(List<Long>?) -> Boolean = { true },
|
||||
) = option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param key The key.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
inline fun <reified T> PatchBuilder<*>.option(
|
||||
key: String,
|
||||
default: T? = null,
|
||||
values: Map<String, T?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
noinline validator: Option<T>.(T?) -> Boolean = { true },
|
||||
) = Option(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
typeOf<T>(),
|
||||
validator,
|
||||
).also { it() }
|
||||
|
||||
/**
|
||||
* An exception thrown when using [Option]s.
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
*/
|
||||
sealed class OptionException(errorMessage: String) : Exception(errorMessage, null) {
|
||||
/**
|
||||
* An exception thrown when a [Option] is set to an invalid value.
|
||||
*
|
||||
* @param invalidType The type of the value that was passed.
|
||||
* @param expectedType The type of the value that was expected.
|
||||
*/
|
||||
class InvalidValueTypeException(invalidType: String, expectedType: String) :
|
||||
OptionException("Type $expectedType was expected but received type $invalidType")
|
||||
|
||||
/**
|
||||
* An exception thrown when a value did not satisfy the value conditions specified by the [Option].
|
||||
*
|
||||
* @param value The value that failed validation.
|
||||
*/
|
||||
class ValueValidationException(value: Any?, option: Option<*>) :
|
||||
OptionException("The option value \"$value\" failed validation for ${option.key}")
|
||||
|
||||
/**
|
||||
* An exception thrown when a value is required but null was passed.
|
||||
*
|
||||
* @param option The [Option] that requires a value.
|
||||
*/
|
||||
class ValueRequiredException(option: Option<*>) :
|
||||
OptionException("The option ${option.key} requires a value, but null was passed")
|
||||
|
||||
/**
|
||||
* An exception thrown when a [Option] is not found.
|
||||
*
|
||||
* @param key The key of the [Option].
|
||||
*/
|
||||
class OptionNotFoundException(key: String) :
|
||||
OptionException("No option with key $key")
|
||||
}
|
||||
@@ -1,133 +1,663 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.PatchClass
|
||||
import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.patch.options.PatchOptions
|
||||
import java.io.Closeable
|
||||
import dalvik.system.DexClassLoader
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarFile
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
typealias PackageName = String
|
||||
typealias VersionName = String
|
||||
typealias Package = Pair<PackageName, Set<VersionName>?>
|
||||
|
||||
/**
|
||||
* A patch.
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
* @param C The [PatchContext] to execute and finalize the patch with.
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* If null, the patch is compatible with all packages.
|
||||
* @param options The options of the patch.
|
||||
* @param executeBlock The execution block of the patch.
|
||||
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
|
||||
* in reverse order of execution.
|
||||
*
|
||||
* @param T The [Context] type this patch will work on.
|
||||
* @constructor Create a new patch.
|
||||
*/
|
||||
sealed class Patch<out T : Context<*>> {
|
||||
/**
|
||||
* The name of the patch.
|
||||
*/
|
||||
var name: String? = null
|
||||
private set
|
||||
sealed class Patch<C : PatchContext<*>>(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val use: Boolean,
|
||||
val dependencies: Set<Patch<*>>,
|
||||
val compatiblePackages: Set<Package>?,
|
||||
options: Set<Option<*>>,
|
||||
private val executeBlock: Patch<C>.(C) -> Unit,
|
||||
// Must be internal and nullable, so that Patcher.invoke can check,
|
||||
// if a patch has a finalizing block in order to not emit it twice.
|
||||
internal var finalizeBlock: (Patch<C>.(C) -> Unit)?,
|
||||
) {
|
||||
val options = Options(options)
|
||||
|
||||
/**
|
||||
* The description of the patch.
|
||||
*/
|
||||
var description: String? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The packages the patch is compatible with.
|
||||
*/
|
||||
var compatiblePackages: Set<CompatiblePackage>? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Other patches this patch depends on.
|
||||
*/
|
||||
var dependencies: Set<PatchClass>? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Weather or not the patch should be used.
|
||||
*/
|
||||
var use = true
|
||||
private set
|
||||
|
||||
// TODO: Remove this property, once integrations are coupled with patches.
|
||||
/**
|
||||
* Weather or not the patch requires integrations.
|
||||
*/
|
||||
var requiresIntegrations = false
|
||||
private set
|
||||
|
||||
constructor(
|
||||
name: String?,
|
||||
description: String?,
|
||||
compatiblePackages: Set<CompatiblePackage>?,
|
||||
dependencies: Set<PatchClass>?,
|
||||
use: Boolean,
|
||||
requiresIntegrations: Boolean,
|
||||
) {
|
||||
this.name = name
|
||||
this.description = description
|
||||
this.compatiblePackages = compatiblePackages
|
||||
this.dependencies = dependencies
|
||||
this.use = use
|
||||
this.requiresIntegrations = requiresIntegrations
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this::class.findAnnotationRecursively(app.revanced.patcher.patch.annotation.Patch::class)?.let { annotation ->
|
||||
this.name = annotation.name.ifEmpty { null }
|
||||
this.description = annotation.description.ifEmpty { null }
|
||||
this.compatiblePackages =
|
||||
annotation.compatiblePackages
|
||||
.map { CompatiblePackage(it.name, it.versions.toSet().ifEmpty { null }) }
|
||||
.toSet().ifEmpty { null }
|
||||
this.dependencies = annotation.dependencies.toSet().ifEmpty { null }
|
||||
this.use = annotation.use
|
||||
this.requiresIntegrations = annotation.requiresIntegrations
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The options of the patch associated by the options key.
|
||||
*/
|
||||
val options = PatchOptions()
|
||||
|
||||
/**
|
||||
* The execution function of the patch.
|
||||
* This function is called by [Patcher].
|
||||
* Runs the execution block of the patch.
|
||||
* Called by [Patcher].
|
||||
*
|
||||
* @param context The [PatcherContext] the patch will work on.
|
||||
* @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with.
|
||||
*/
|
||||
internal abstract fun execute(context: PatcherContext)
|
||||
|
||||
/**
|
||||
* The execution function of the patch.
|
||||
* Runs the execution block of the patch.
|
||||
*
|
||||
* @param context The [Context] the patch will work on.
|
||||
* @return The result of executing the patch.
|
||||
* @param context The [PatchContext] to execute the patch with.
|
||||
*/
|
||||
abstract fun execute(context: @UnsafeVariance T)
|
||||
fun execute(context: C) = executeBlock(context)
|
||||
|
||||
override fun hashCode() = name.hashCode()
|
||||
/**
|
||||
* Runs the finalizing block of the patch.
|
||||
* Called by [Patcher].
|
||||
*
|
||||
* @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with.
|
||||
*/
|
||||
internal abstract fun finalize(context: PatcherContext)
|
||||
|
||||
override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch"
|
||||
/**
|
||||
* Runs the finalizing block of the patch.
|
||||
*
|
||||
* @param context The [PatchContext] to finalize the patch with.
|
||||
*/
|
||||
fun finalize(context: C) {
|
||||
finalizeBlock?.invoke(this, context)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
override fun toString() = name ?: "Patch"
|
||||
}
|
||||
|
||||
other as Patch<*>
|
||||
/**
|
||||
* A bytecode patch.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* If null, the patch is compatible with all packages.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param options The options of the patch.
|
||||
* @param fingerprints The fingerprints that are resolved before the patch is executed.
|
||||
* @property extension An input stream of the extension resource this patch uses.
|
||||
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
|
||||
* @param executeBlock The execution block of the patch.
|
||||
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
|
||||
* in reverse order of execution.
|
||||
*
|
||||
* @constructor Create a new bytecode patch.
|
||||
*/
|
||||
class BytecodePatch internal constructor(
|
||||
name: String?,
|
||||
description: String?,
|
||||
use: Boolean,
|
||||
compatiblePackages: Set<Package>?,
|
||||
dependencies: Set<Patch<*>>,
|
||||
options: Set<Option<*>>,
|
||||
val fingerprints: Set<Fingerprint>,
|
||||
val extension: InputStream?,
|
||||
executeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit,
|
||||
finalizeBlock: (Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit)?,
|
||||
) : Patch<BytecodePatchContext>(
|
||||
name,
|
||||
description,
|
||||
use,
|
||||
dependencies,
|
||||
compatiblePackages,
|
||||
options,
|
||||
executeBlock,
|
||||
finalizeBlock,
|
||||
) {
|
||||
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
|
||||
extension?.let(::merge)
|
||||
fingerprints.forEach { it.match(this) }
|
||||
|
||||
return name == other.name
|
||||
execute(this)
|
||||
}
|
||||
|
||||
override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext)
|
||||
|
||||
override fun toString() = name ?: "BytecodePatch"
|
||||
}
|
||||
|
||||
/**
|
||||
* A raw resource patch.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* If null, the patch is compatible with all packages.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param options The options of the patch.
|
||||
* @param executeBlock The execution block of the patch.
|
||||
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
|
||||
* in reverse order of execution.
|
||||
*
|
||||
* @constructor Create a new raw resource patch.
|
||||
*/
|
||||
class RawResourcePatch internal constructor(
|
||||
name: String?,
|
||||
description: String?,
|
||||
use: Boolean,
|
||||
compatiblePackages: Set<Package>?,
|
||||
dependencies: Set<Patch<*>>,
|
||||
options: Set<Option<*>>,
|
||||
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
|
||||
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
|
||||
) : Patch<ResourcePatchContext>(
|
||||
name,
|
||||
description,
|
||||
use,
|
||||
dependencies,
|
||||
compatiblePackages,
|
||||
options,
|
||||
executeBlock,
|
||||
finalizeBlock,
|
||||
) {
|
||||
override fun execute(context: PatcherContext) = execute(context.resourceContext)
|
||||
|
||||
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
|
||||
|
||||
override fun toString() = name ?: "RawResourcePatch"
|
||||
}
|
||||
|
||||
/**
|
||||
* A resource patch.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* If null, the patch is compatible with all packages.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param options The options of the patch.
|
||||
* @param executeBlock The execution block of the patch.
|
||||
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
|
||||
* in reverse order of execution.
|
||||
*
|
||||
* @constructor Create a new resource patch.
|
||||
*/
|
||||
class ResourcePatch internal constructor(
|
||||
name: String?,
|
||||
description: String?,
|
||||
use: Boolean,
|
||||
compatiblePackages: Set<Package>?,
|
||||
dependencies: Set<Patch<*>>,
|
||||
options: Set<Option<*>>,
|
||||
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
|
||||
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
|
||||
) : Patch<ResourcePatchContext>(
|
||||
name,
|
||||
description,
|
||||
use,
|
||||
dependencies,
|
||||
compatiblePackages,
|
||||
options,
|
||||
executeBlock,
|
||||
finalizeBlock,
|
||||
) {
|
||||
override fun execute(context: PatcherContext) = execute(context.resourceContext)
|
||||
|
||||
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
|
||||
|
||||
override fun toString() = name ?: "ResourcePatch"
|
||||
}
|
||||
|
||||
/**
|
||||
* A [Patch] builder.
|
||||
*
|
||||
* @param C The [PatchContext] to execute and finalize the patch with.
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @property compatiblePackages The packages the patch is compatible with.
|
||||
* If null, the patch is compatible with all packages.
|
||||
* @property dependencies Other patches this patch depends on.
|
||||
* @property options The options of the patch.
|
||||
* @property executionBlock The execution block of the patch.
|
||||
* @property finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
|
||||
* in reverse order of execution.
|
||||
*
|
||||
* @constructor Create a new [Patch] builder.
|
||||
*/
|
||||
sealed class PatchBuilder<C : PatchContext<*>>(
|
||||
protected val name: String?,
|
||||
protected val description: String?,
|
||||
protected val use: Boolean,
|
||||
) {
|
||||
protected var compatiblePackages: MutableSet<Package>? = null
|
||||
protected var dependencies = mutableSetOf<Patch<*>>()
|
||||
protected val options = mutableSetOf<Option<*>>()
|
||||
|
||||
protected var executionBlock: (Patch<C>.(C) -> Unit) = { }
|
||||
protected var finalizeBlock: (Patch<C>.(C) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* Add an option to the patch.
|
||||
*
|
||||
* @return The added option.
|
||||
*/
|
||||
operator fun <T> Option<T>.invoke() = apply {
|
||||
options += this
|
||||
}
|
||||
|
||||
/**
|
||||
* A package a [Patch] is compatible with.
|
||||
* Create a package a patch is compatible with.
|
||||
*
|
||||
* @param name The name of the package.
|
||||
* @param versions The versions of the package.
|
||||
*/
|
||||
class CompatiblePackage(
|
||||
val name: String,
|
||||
val versions: Set<String>? = null,
|
||||
operator fun String.invoke(vararg versions: String) = this to versions.toSet()
|
||||
|
||||
/**
|
||||
* Add packages the patch is compatible with.
|
||||
*
|
||||
* @param packages The packages the patch is compatible with.
|
||||
*/
|
||||
fun compatibleWith(vararg packages: Package) {
|
||||
if (compatiblePackages == null) {
|
||||
compatiblePackages = mutableSetOf()
|
||||
}
|
||||
|
||||
compatiblePackages!! += packages
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the compatible packages of the patch.
|
||||
*
|
||||
* @param packages The packages the patch is compatible with.
|
||||
*/
|
||||
fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray())
|
||||
|
||||
/**
|
||||
* Add dependencies to the patch.
|
||||
*
|
||||
* @param patches The patches the patch depends on.
|
||||
*/
|
||||
fun dependsOn(vararg patches: Patch<*>) {
|
||||
dependencies += patches
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the execution block of the patch.
|
||||
*
|
||||
* @param block The execution block of the patch.
|
||||
*/
|
||||
fun execute(block: Patch<C>.(C) -> Unit) {
|
||||
executionBlock = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the finalizing block of the patch.
|
||||
*
|
||||
* @param block The finalizing block of the patch.
|
||||
*/
|
||||
fun finalize(block: Patch<C>.(C) -> Unit) {
|
||||
finalizeBlock = block
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the patch.
|
||||
*
|
||||
* @return The built patch.
|
||||
*/
|
||||
internal abstract fun build(): Patch<C>
|
||||
}
|
||||
|
||||
/**
|
||||
* A [BytecodePatchBuilder] builder.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @property fingerprints The fingerprints that are resolved before the patch is executed.
|
||||
* @property extension An input stream of the extension resource this patch uses.
|
||||
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
|
||||
*
|
||||
* @constructor Create a new [BytecodePatchBuilder] builder.
|
||||
*/
|
||||
class BytecodePatchBuilder internal constructor(
|
||||
name: String?,
|
||||
description: String?,
|
||||
use: Boolean,
|
||||
) : PatchBuilder<BytecodePatchContext>(name, description, use) {
|
||||
private val fingerprints = mutableSetOf<Fingerprint>()
|
||||
|
||||
/**
|
||||
* Add the fingerprint to the patch.
|
||||
*
|
||||
* @return A wrapper for the fingerprint with the ability to delegate the match to the fingerprint.
|
||||
*/
|
||||
operator fun Fingerprint.invoke() = InvokedFingerprint(also { fingerprints.add(it) })
|
||||
|
||||
class InvokedFingerprint(private val fingerprint: Fingerprint) {
|
||||
// The reason getValue isn't extending the Fingerprint class is
|
||||
// because delegating makes only sense if the fingerprint was previously added to the patch by invoking it.
|
||||
// It may be likely to forget invoking it. By wrapping the fingerprint into this class,
|
||||
// the compiler will throw an error if the fingerprint was not invoked if attempting to delegate the match.
|
||||
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = fingerprint.match
|
||||
?: throw PatchException("No fingerprint match to delegate to ${property.name}.")
|
||||
}
|
||||
|
||||
// Must be internal for the inlined function "extendWith".
|
||||
@PublishedApi
|
||||
internal var extension: InputStream? = null
|
||||
|
||||
// Inlining is necessary to get the class loader that loaded the patch
|
||||
// to load the extension from the resources.
|
||||
/**
|
||||
* Set the extension of the patch.
|
||||
*
|
||||
* @param extension The name of the extension resource.
|
||||
*/
|
||||
inline fun extendWith(extension: String) = apply {
|
||||
this.extension = object {}.javaClass.classLoader.getResourceAsStream(extension)
|
||||
?: throw PatchException("Extension resource \"$extension\" not found")
|
||||
}
|
||||
|
||||
override fun build() = BytecodePatch(
|
||||
name,
|
||||
description,
|
||||
use,
|
||||
compatiblePackages,
|
||||
dependencies,
|
||||
options,
|
||||
fingerprints,
|
||||
extension,
|
||||
executionBlock,
|
||||
finalizeBlock,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [RawResourcePatch] builder.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
*
|
||||
* @constructor Create a new [RawResourcePatch] builder.
|
||||
*/
|
||||
class RawResourcePatchBuilder internal constructor(
|
||||
name: String?,
|
||||
description: String?,
|
||||
use: Boolean,
|
||||
) : PatchBuilder<ResourcePatchContext>(name, description, use) {
|
||||
override fun build() = RawResourcePatch(
|
||||
name,
|
||||
description,
|
||||
use,
|
||||
compatiblePackages,
|
||||
dependencies,
|
||||
options,
|
||||
executionBlock,
|
||||
finalizeBlock,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A [ResourcePatch] builder.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
*
|
||||
* @constructor Create a new [ResourcePatch] builder.
|
||||
*/
|
||||
class ResourcePatchBuilder internal constructor(
|
||||
name: String?,
|
||||
description: String?,
|
||||
use: Boolean,
|
||||
) : PatchBuilder<ResourcePatchContext>(name, description, use) {
|
||||
override fun build() = ResourcePatch(
|
||||
name,
|
||||
description,
|
||||
use,
|
||||
compatiblePackages,
|
||||
dependencies,
|
||||
options,
|
||||
executionBlock,
|
||||
finalizeBlock,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a [Patch].
|
||||
*
|
||||
* @param B The [PatchBuilder] to build the patch with.
|
||||
* @param block The block to build the patch.
|
||||
*
|
||||
* @return The built [Patch].
|
||||
*/
|
||||
private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply(block).build()
|
||||
|
||||
/**
|
||||
* Create a new [BytecodePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param block The block to build the patch.
|
||||
*
|
||||
* @return The created [BytecodePatch].
|
||||
*/
|
||||
fun bytecodePatch(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: BytecodePatchBuilder.() -> Unit = {},
|
||||
) = BytecodePatchBuilder(name, description, use).buildPatch(block) as BytecodePatch
|
||||
|
||||
/**
|
||||
* Create a new [RawResourcePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param block The block to build the patch.
|
||||
* @return The created [RawResourcePatch].
|
||||
*/
|
||||
fun rawResourcePatch(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: RawResourcePatchBuilder.() -> Unit = {},
|
||||
) = RawResourcePatchBuilder(name, description, use).buildPatch(block) as RawResourcePatch
|
||||
|
||||
/**
|
||||
* Create a new [ResourcePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
* @param description The description of the patch.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param block The block to build the patch.
|
||||
*
|
||||
* @return The created [ResourcePatch].
|
||||
*/
|
||||
fun resourcePatch(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: ResourcePatchBuilder.() -> Unit = {},
|
||||
) = ResourcePatchBuilder(name, description, use).buildPatch(block) as ResourcePatch
|
||||
|
||||
/**
|
||||
* An exception thrown when patching.
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
* @param cause The corresponding [Throwable].
|
||||
*/
|
||||
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
|
||||
constructor(errorMessage: String) : this(errorMessage, null)
|
||||
constructor(cause: Throwable) : this(cause.message, cause)
|
||||
}
|
||||
|
||||
/**
|
||||
* A result of executing a [Patch].
|
||||
*
|
||||
* @param patch The [Patch] that was executed.
|
||||
* @param exception The [PatchException] thrown, if any.
|
||||
*/
|
||||
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
|
||||
|
||||
/**
|
||||
* A loader for patches.
|
||||
*
|
||||
* Loads unnamed patches from JAR or DEX files declared as public static fields
|
||||
* or returned by public static and non-parametrized methods.
|
||||
*
|
||||
* @param byPatchesFile The patches associated by the patches file they were loaded from.
|
||||
*/
|
||||
sealed class PatchLoader private constructor(
|
||||
val byPatchesFile: Map<File, Set<Patch<*>>>,
|
||||
) : Set<Patch<*>> by byPatchesFile.values.flatten().toSet() {
|
||||
/**
|
||||
* @param patchesFiles A set of JAR or DEX files to load the patches from.
|
||||
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
|
||||
* @param classLoader The [ClassLoader] to use for loading the classes.
|
||||
*/
|
||||
private constructor(
|
||||
patchesFiles: Set<File>,
|
||||
getBinaryClassNames: (patchesFile: File) -> List<String>,
|
||||
classLoader: ClassLoader,
|
||||
) : this(classLoader.loadPatches(patchesFiles.associateWith { getBinaryClassNames(it).toSet() }))
|
||||
|
||||
/**
|
||||
* A [PatchLoader] for JAR files.
|
||||
*
|
||||
* @param patchesFiles The JAR files to load the patches from.
|
||||
*
|
||||
* @constructor Create a new [PatchLoader] for JAR files.
|
||||
*/
|
||||
class Jar(patchesFiles: Set<File>) :
|
||||
PatchLoader(
|
||||
patchesFiles,
|
||||
{ file ->
|
||||
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
|
||||
.map { it.name.substringBeforeLast('.').replace('/', '.') }
|
||||
},
|
||||
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
|
||||
)
|
||||
|
||||
/**
|
||||
* A [PatchLoader] for [Dex] files.
|
||||
*
|
||||
* @param patchesFiles The DEX files to load the patches from.
|
||||
* @param optimizedDexDirectory The directory to store optimized DEX files in.
|
||||
* This parameter is deprecated and has no effect since API level 26.
|
||||
*
|
||||
* @constructor Create a new [PatchLoader] for [Dex] files.
|
||||
*/
|
||||
class Dex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) :
|
||||
PatchLoader(
|
||||
patchesFiles,
|
||||
{ patchBundle ->
|
||||
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
|
||||
.map { classDef ->
|
||||
classDef.type.substring(1, classDef.length - 1)
|
||||
}
|
||||
},
|
||||
DexClassLoader(
|
||||
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||
optimizedDexDirectory?.absolutePath,
|
||||
null,
|
||||
this::class.java.classLoader,
|
||||
),
|
||||
)
|
||||
|
||||
// Companion object required for unit tests.
|
||||
private companion object {
|
||||
val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
|
||||
|
||||
/**
|
||||
* Public static fields that are patches.
|
||||
*/
|
||||
private val Class<*>.patchFields
|
||||
get() = fields.filter { field ->
|
||||
field.type.isPatch && field.canAccess(null)
|
||||
}.map { field ->
|
||||
field.get(null) as Patch<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Public static and non-parametrized methods that return patches.
|
||||
*/
|
||||
private val Class<*>.patchMethods
|
||||
get() = methods.filter { method ->
|
||||
method.returnType.isPatch && method.parameterCount == 0 && method.canAccess(null)
|
||||
}.map { method ->
|
||||
method.invoke(null) as Patch<*>
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads unnamed patches declared as public static fields
|
||||
* or returned by public static and non-parametrized methods.
|
||||
*
|
||||
* @param binaryClassNamesByPatchesFile The binary class name of the classes to load the patches from
|
||||
* associated by the patches file.
|
||||
*
|
||||
* @return The loaded patches associated by the patches file.
|
||||
*/
|
||||
private fun ClassLoader.loadPatches(binaryClassNamesByPatchesFile: Map<File, Set<String>>) =
|
||||
binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) ->
|
||||
binaryClassNames.asSequence().map {
|
||||
loadClass(it)
|
||||
}.flatMap {
|
||||
it.patchFields + it.patchMethods
|
||||
}.filter {
|
||||
it.name != null
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads patches from JAR files declared as public static fields
|
||||
* or returned by public static and non-parametrized methods.
|
||||
* Patches with no name are not loaded.
|
||||
*
|
||||
* @param patchesFiles The JAR files to load the patches from.
|
||||
*
|
||||
* @return The loaded patches.
|
||||
*/
|
||||
fun loadPatchesFromJar(patchesFiles: Set<File>) =
|
||||
PatchLoader.Jar(patchesFiles)
|
||||
|
||||
/**
|
||||
* Loads patches from DEX files declared as public static fields
|
||||
* or returned by public static and non-parametrized methods.
|
||||
* Patches with no name are not loaded.
|
||||
*
|
||||
* @param patchesFiles The DEX files to load the patches from.
|
||||
*
|
||||
* @return The loaded patches.
|
||||
*/
|
||||
fun loadPatchesFromDex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) =
|
||||
PatchLoader.Dex(patchesFiles, optimizedDexDirectory)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
|
||||
*/
|
||||
|
||||
sealed interface PatchContext<T> : Supplier<T>
|
||||
@@ -1,12 +0,0 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
/**
|
||||
* An exception thrown when patching.
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
* @param cause The corresponding [Throwable].
|
||||
*/
|
||||
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
|
||||
constructor(errorMessage: String) : this(errorMessage, null)
|
||||
constructor(cause: Throwable) : this(cause.message, cause)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
/**
|
||||
* A result of executing a [Patch].
|
||||
*
|
||||
* @param patch The [Patch] that was executed.
|
||||
* @param exception The [PatchException] thrown, if any.
|
||||
*/
|
||||
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
|
||||
@@ -1,46 +0,0 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.PatchClass
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* A [Patch] that accesses a [ResourceContext].
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
*
|
||||
* This type of patch that does not have access to decoded resources.
|
||||
* Instead, you can read and write arbitrary files in an APK file.
|
||||
*
|
||||
* If you want to access decoded resources, use [ResourcePatch] instead.
|
||||
*/
|
||||
abstract class RawResourcePatch : Patch<ResourceContext> {
|
||||
/**
|
||||
* Create a new [RawResourcePatch].
|
||||
*/
|
||||
constructor()
|
||||
|
||||
/**
|
||||
* Create a new [RawResourcePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* @param description The description of the patch.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||
*/
|
||||
constructor(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
compatiblePackages: Set<CompatiblePackage>? = null,
|
||||
dependencies: Set<PatchClass>? = null,
|
||||
use: Boolean = true,
|
||||
requiresIntegrations: Boolean = false,
|
||||
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
|
||||
|
||||
override fun execute(context: PatcherContext) = execute(context.resourceContext)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.PatchClass
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* A [Patch] that accesses a [ResourceContext].
|
||||
*
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by [Patcher].
|
||||
*
|
||||
* This type of patch has access to decoded resources.
|
||||
* Additionally, you can read and write arbitrary files in an APK file.
|
||||
*
|
||||
* If you do not need access to decoded resources, use [RawResourcePatch] instead.
|
||||
*/
|
||||
abstract class ResourcePatch : Patch<ResourceContext> {
|
||||
/**
|
||||
* Create a new [ResourcePatch].
|
||||
*/
|
||||
constructor()
|
||||
|
||||
/**
|
||||
* Create a new [ResourcePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* @param description The description of the patch.
|
||||
* @param compatiblePackages The packages the patch is compatible with.
|
||||
* @param dependencies Other patches this patch depends on.
|
||||
* @param use Weather or not the patch should be used.
|
||||
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||
*/
|
||||
constructor(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
compatiblePackages: Set<CompatiblePackage>? = null,
|
||||
dependencies: Set<PatchClass>? = null,
|
||||
use: Boolean = true,
|
||||
requiresIntegrations: Boolean = false,
|
||||
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
|
||||
|
||||
override fun execute(context: PatcherContext) = execute(context.resourceContext)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package app.revanced.patcher.data
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.InternalApi
|
||||
import app.revanced.patcher.PackageMetadata
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.util.Document
|
||||
import app.revanced.patcher.util.DomFileEditor
|
||||
import brut.androlib.AaptInvoker
|
||||
import brut.androlib.ApkDecoder
|
||||
import brut.androlib.apk.UsesFramework
|
||||
@@ -15,33 +14,28 @@ import brut.androlib.res.decoder.AndroidManifestResourceParser
|
||||
import brut.androlib.res.decoder.XmlPullStreamDecoder
|
||||
import brut.androlib.res.xml.ResXmlPatcher
|
||||
import brut.directory.ExtFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for the patcher containing the current state of the resources.
|
||||
* A context for patches containing the current state of resources.
|
||||
*
|
||||
* @param packageMetadata The [PackageMetadata] of the apk file.
|
||||
* @param config The [PatcherConfig] used to create this context.
|
||||
*/
|
||||
class ResourceContext internal constructor(
|
||||
class ResourcePatchContext internal constructor(
|
||||
private val packageMetadata: PackageMetadata,
|
||||
private val config: PatcherConfig,
|
||||
) : Context<PatcherResult.PatchedResources?>, Iterable<File> {
|
||||
private val logger = Logger.getLogger(ResourceContext::class.java.name)
|
||||
) : PatchContext<PatcherResult.PatchedResources?> {
|
||||
private val logger = Logger.getLogger(ResourcePatchContext::class.java.name)
|
||||
|
||||
/**
|
||||
* Read and write documents in the [PatcherConfig.apkFiles].
|
||||
*/
|
||||
val document = DocumentOperatable()
|
||||
|
||||
@Deprecated("Use document instead.")
|
||||
@Suppress("DEPRECATION")
|
||||
val xmlEditor = XmlFileHolder()
|
||||
|
||||
/**
|
||||
* Predicate to delete resources from [PatcherConfig.apkFiles].
|
||||
*/
|
||||
@@ -155,7 +149,7 @@ class ResourceContext internal constructor(
|
||||
// Excluded because present in resources.other.
|
||||
// TODO: We are reusing config.apkFiles as a temporarily directory for extracting resources.
|
||||
// This is not ideal as it could conflict with files such as the ones that we filter here.
|
||||
// The problem is that ResourceContext#get returns a File relative to config.apkFiles,
|
||||
// The problem is that ResourcePatchContext#get returns a File relative to config.apkFiles,
|
||||
// and we need to extract files to that directory.
|
||||
// A solution would be to use config.apkFiles as the working directory for the patching process.
|
||||
// Once all patches have been executed, we can move the decoded resources to a new directory.
|
||||
@@ -213,12 +207,6 @@ class ResourceContext internal constructor(
|
||||
*/
|
||||
fun stageDelete(shouldDelete: (String) -> Boolean) = deleteResources.add(shouldDelete)
|
||||
|
||||
@Deprecated("Use get(String, Boolean) instead.", ReplaceWith("get(path, false)"))
|
||||
operator fun get(path: String) = get(path, false)
|
||||
|
||||
@Deprecated("Use get(String, Boolean) instead.")
|
||||
override fun iterator(): Iterator<File> = config.apkFiles.listFiles()!!.iterator()
|
||||
|
||||
/**
|
||||
* How to handle resources decoding and compiling.
|
||||
*/
|
||||
@@ -243,18 +231,6 @@ class ResourceContext internal constructor(
|
||||
inner class DocumentOperatable {
|
||||
operator fun get(inputStream: InputStream) = Document(inputStream)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
operator fun get(path: String) = Document(this@ResourceContext[path])
|
||||
}
|
||||
|
||||
@Deprecated("Use DocumentOperatable instead.")
|
||||
inner class XmlFileHolder {
|
||||
@Suppress("DEPRECATION")
|
||||
operator fun get(inputStream: InputStream) = DomFileEditor(inputStream)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
operator fun get(path: String): DomFileEditor {
|
||||
return DomFileEditor(this@ResourceContext[path])
|
||||
}
|
||||
operator fun get(path: String) = Document(this@ResourcePatchContext[path])
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package app.revanced.patcher.patch.annotation
|
||||
|
||||
import java.lang.annotation.Inherited
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Annotation for [app.revanced.patcher.patch.Patch] classes.
|
||||
*
|
||||
* @param name The name of the patch. If empty, the patch will be unnamed.
|
||||
* @param description The description of the patch. If empty, no description will be used.
|
||||
* @param dependencies The patches this patch depends on.
|
||||
* @param compatiblePackages The packages this patch is compatible with.
|
||||
* @param use Whether this patch should be used.
|
||||
* @param requiresIntegrations Whether this patch requires integrations.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Inherited
|
||||
annotation class Patch(
|
||||
val name: String = "",
|
||||
val description: String = "",
|
||||
val dependencies: Array<KClass<out app.revanced.patcher.patch.Patch<*>>> = [],
|
||||
val compatiblePackages: Array<CompatiblePackage> = [],
|
||||
val use: Boolean = true,
|
||||
// TODO: Remove this property, once integrations are coupled with patches.
|
||||
val requiresIntegrations: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* A package that a [app.revanced.patcher.patch.Patch] is compatible with.
|
||||
*
|
||||
* @param name The name of the package.
|
||||
* @param versions The versions of the package.
|
||||
*/
|
||||
annotation class CompatiblePackage(
|
||||
val name: String,
|
||||
val versions: Array<String> = [],
|
||||
)
|
||||
@@ -1,476 +0,0 @@
|
||||
package app.revanced.patcher.patch.options
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* A [Patch] option.
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values identified by their string representation.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param valueType The type of the option value (to handle type erasure).
|
||||
* @param validator The function to validate the option value.
|
||||
* @param T The value type of the option.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
open class PatchOption<T>(
|
||||
val key: String,
|
||||
val default: T?,
|
||||
val values: Map<String, T?>?,
|
||||
val title: String?,
|
||||
val description: String?,
|
||||
val required: Boolean,
|
||||
val valueType: String,
|
||||
val validator: PatchOption<T>.(T?) -> Boolean,
|
||||
) {
|
||||
/**
|
||||
* The value of the [PatchOption].
|
||||
*/
|
||||
var value: T?
|
||||
/**
|
||||
* Set the value of the [PatchOption].
|
||||
*
|
||||
* @param value The value to set.
|
||||
*
|
||||
* @throws PatchOptionException.ValueRequiredException If the value is required but null.
|
||||
* @throws PatchOptionException.ValueValidationException If the value is invalid.
|
||||
*/
|
||||
set(value) {
|
||||
assertRequiredButNotNull(value)
|
||||
assertValid(value)
|
||||
|
||||
uncheckedValue = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the [PatchOption].
|
||||
*
|
||||
* @return The value.
|
||||
*
|
||||
* @throws PatchOptionException.ValueRequiredException If the value is required but null.
|
||||
* @throws PatchOptionException.ValueValidationException If the value is invalid.
|
||||
*/
|
||||
get() {
|
||||
assertRequiredButNotNull(uncheckedValue)
|
||||
assertValid(uncheckedValue)
|
||||
|
||||
return uncheckedValue
|
||||
}
|
||||
|
||||
// The unchecked value is used to allow setting the value without validation.
|
||||
private var uncheckedValue = default
|
||||
|
||||
/**
|
||||
* Reset the [PatchOption] to its default value.
|
||||
* Override this method if you need to mutate the value instead of replacing it.
|
||||
*/
|
||||
open fun reset() {
|
||||
uncheckedValue = default
|
||||
}
|
||||
|
||||
private fun assertRequiredButNotNull(value: T?) {
|
||||
if (required && value == null) throw PatchOptionException.ValueRequiredException(this)
|
||||
}
|
||||
|
||||
private fun assertValid(value: T?) {
|
||||
if (!validator(value)) throw PatchOptionException.ValueValidationException(value, this)
|
||||
}
|
||||
|
||||
override fun toString() = value.toString()
|
||||
|
||||
operator fun getValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
) = value
|
||||
|
||||
operator fun setValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
value: T?,
|
||||
) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
companion object PatchExtensions {
|
||||
/**
|
||||
* Create a new [PatchOption] with a string value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.stringPatchOption(
|
||||
key: String,
|
||||
default: String? = null,
|
||||
values: Map<String, String?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<String>.(String?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"String",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with an integer value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.intPatchOption(
|
||||
key: String,
|
||||
default: Int? = null,
|
||||
values: Map<String, Int?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Int?>.(Int?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"Int",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with a boolean value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.booleanPatchOption(
|
||||
key: String,
|
||||
default: Boolean? = null,
|
||||
values: Map<String, Boolean?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Boolean?>.(Boolean?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"Boolean",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with a float value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.floatPatchOption(
|
||||
key: String,
|
||||
default: Float? = null,
|
||||
values: Map<String, Float?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Float?>.(Float?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"Float",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with a long value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.longPatchOption(
|
||||
key: String,
|
||||
default: Long? = null,
|
||||
values: Map<String, Long?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Long?>.(Long?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"Long",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with a string array value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.stringArrayPatchOption(
|
||||
key: String,
|
||||
default: Array<String>? = null,
|
||||
values: Map<String, Array<String>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Array<String>?>.(Array<String>?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"StringArray",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with an integer array value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.intArrayPatchOption(
|
||||
key: String,
|
||||
default: Array<Int>? = null,
|
||||
values: Map<String, Array<Int>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Array<Int>?>.(Array<Int>?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"IntArray",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with a boolean array value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.booleanArrayPatchOption(
|
||||
key: String,
|
||||
default: Array<Boolean>? = null,
|
||||
values: Map<String, Array<Boolean>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Array<Boolean>?>.(Array<Boolean>?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"BooleanArray",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with a float array value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.floatArrayPatchOption(
|
||||
key: String,
|
||||
default: Array<Float>? = null,
|
||||
values: Map<String, Array<Float>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Array<Float>?>.(Array<Float>?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"FloatArray",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] with a long array value and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>> P.longArrayPatchOption(
|
||||
key: String,
|
||||
default: Array<Long>? = null,
|
||||
values: Map<String, Array<Long>?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: PatchOption<Array<Long>?>.(Array<Long>?) -> Boolean = { true },
|
||||
) = registerNewPatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
"LongArray",
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [PatchOption] and add it to the current [Patch].
|
||||
*
|
||||
* @param key The identifier.
|
||||
* @param default The default value.
|
||||
* @param values The set of guaranteed valid values identified by their string representation.
|
||||
* @param title The title.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param valueType The type of the option value (to handle type erasure).
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [PatchOption].
|
||||
*
|
||||
* @see PatchOption
|
||||
*/
|
||||
fun <P : Patch<*>, T> P.registerNewPatchOption(
|
||||
key: String,
|
||||
default: T? = null,
|
||||
values: Map<String, T?>? = null,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
valueType: String,
|
||||
validator: PatchOption<T>.(T?) -> Boolean = { true },
|
||||
) = PatchOption(
|
||||
key,
|
||||
default,
|
||||
values,
|
||||
title,
|
||||
description,
|
||||
required,
|
||||
valueType,
|
||||
validator,
|
||||
).also(options::register)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package app.revanced.patcher.patch.options
|
||||
|
||||
/**
|
||||
* An exception thrown when using [PatchOption]s.
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
*/
|
||||
sealed class PatchOptionException(errorMessage: String) : Exception(errorMessage, null) {
|
||||
/**
|
||||
* An exception thrown when a [PatchOption] is set to an invalid value.
|
||||
*
|
||||
* @param invalidType The type of the value that was passed.
|
||||
* @param expectedType The type of the value that was expected.
|
||||
*/
|
||||
class InvalidValueTypeException(invalidType: String, expectedType: String) :
|
||||
PatchOptionException("Type $expectedType was expected but received type $invalidType")
|
||||
|
||||
/**
|
||||
* An exception thrown when a value did not satisfy the value conditions specified by the [PatchOption].
|
||||
*
|
||||
* @param value The value that failed validation.
|
||||
*/
|
||||
class ValueValidationException(value: Any?, option: PatchOption<*>) :
|
||||
PatchOptionException("The option value \"$value\" failed validation for ${option.key}")
|
||||
|
||||
/**
|
||||
* An exception thrown when a value is required but null was passed.
|
||||
*
|
||||
* @param option The [PatchOption] that requires a value.
|
||||
*/
|
||||
class ValueRequiredException(option: PatchOption<*>) :
|
||||
PatchOptionException("The option ${option.key} requires a value, but null was passed")
|
||||
|
||||
/**
|
||||
* An exception thrown when a [PatchOption] is not found.
|
||||
*
|
||||
* @param key The key of the [PatchOption].
|
||||
*/
|
||||
class PatchOptionNotFoundException(key: String) :
|
||||
PatchOptionException("No option with key $key")
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package app.revanced.patcher.patch.options
|
||||
|
||||
/**
|
||||
* A map of [PatchOption]s associated by their keys.
|
||||
*
|
||||
* @param options The [PatchOption]s to initialize with.
|
||||
*/
|
||||
class PatchOptions internal constructor(
|
||||
private val options: MutableMap<String, PatchOption<*>> = mutableMapOf(),
|
||||
) : MutableMap<String, PatchOption<*>> by options {
|
||||
/**
|
||||
* Register a [PatchOption]. Acts like [MutableMap.put].
|
||||
* @param value The [PatchOption] to register.
|
||||
*/
|
||||
fun register(value: PatchOption<*>) {
|
||||
options[value.key] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an option's value.
|
||||
* @param key The identifier.
|
||||
* @param value The value.
|
||||
* @throws PatchOptionException.PatchOptionNotFoundException If the option does not exist.
|
||||
*/
|
||||
operator fun <T : Any> set(
|
||||
key: String,
|
||||
value: T?,
|
||||
) {
|
||||
val option = this[key]
|
||||
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(option as PatchOption<T>).value = value
|
||||
} catch (e: ClassCastException) {
|
||||
throw PatchOptionException.InvalidValueTypeException(
|
||||
value?.let { it::class.java.name } ?: "null",
|
||||
option.value?.let { it::class.java.name } ?: "null",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option.
|
||||
*/
|
||||
override operator fun get(key: String) = options[key] ?: throw PatchOptionException.PatchOptionNotFoundException(key)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.extensions.or
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.filterAny
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
|
||||
@@ -36,7 +35,7 @@ internal object ClassMerger {
|
||||
*/
|
||||
fun ClassDef.merge(
|
||||
otherClass: ClassDef,
|
||||
context: BytecodeContext,
|
||||
context: BytecodePatchContext,
|
||||
) = this
|
||||
// .fixFieldAccess(otherClass)
|
||||
// .fixMethodAccess(otherClass)
|
||||
@@ -95,7 +94,7 @@ internal object ClassMerger {
|
||||
*/
|
||||
private fun ClassDef.publicize(
|
||||
reference: ClassDef,
|
||||
context: BytecodeContext,
|
||||
context: BytecodePatchContext,
|
||||
) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
|
||||
this.asMutableClass().apply {
|
||||
context.traverseClassHierarchy(this) {
|
||||
@@ -175,12 +174,12 @@ internal object ClassMerger {
|
||||
* @param targetClass the class to start traversing the class hierarchy from
|
||||
* @param callback function that is called for every class in the hierarchy
|
||||
*/
|
||||
fun BytecodeContext.traverseClassHierarchy(
|
||||
fun BytecodePatchContext.traverseClassHierarchy(
|
||||
targetClass: MutableClass,
|
||||
callback: MutableClass.() -> Unit,
|
||||
) {
|
||||
callback(targetClass)
|
||||
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
|
||||
this.classByType(targetClass.superclass ?: return)?.mutableClass?.let {
|
||||
traverseClassHierarchy(it, callback)
|
||||
}
|
||||
}
|
||||
@@ -199,7 +198,7 @@ internal object ClassMerger {
|
||||
*
|
||||
* @return The new [AccessFlags].
|
||||
*/
|
||||
fun Int.toPublic() = this.or(AccessFlags.PUBLIC).and(AccessFlags.PRIVATE.value.inv())
|
||||
fun Int.toPublic() = or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
|
||||
|
||||
/**
|
||||
* Filter [this] on [needles] matching the given [predicate].
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import org.w3c.dom.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
@Deprecated("Use Document instead.")
|
||||
class DomFileEditor : Closeable {
|
||||
val file: Document
|
||||
internal constructor(
|
||||
inputStream: InputStream,
|
||||
) {
|
||||
file = Document(inputStream)
|
||||
}
|
||||
|
||||
constructor(file: File) {
|
||||
this.file = Document(file)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
file as app.revanced.patcher.util.Document
|
||||
file.close()
|
||||
}
|
||||
}
|
||||
109
src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt
Normal file
109
src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.util.MethodNavigator.NavigateException
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
/**
|
||||
* A navigator for methods.
|
||||
*
|
||||
* @param context The [BytecodePatchContext] to use.
|
||||
* @param startMethod The [Method] to start navigating from.
|
||||
*
|
||||
* @constructor Creates a new [MethodNavigator].
|
||||
*
|
||||
* @throws NavigateException If the method does not have an implementation.
|
||||
* @throws NavigateException If the instruction at the specified index is not a method reference.
|
||||
*/
|
||||
class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) {
|
||||
private var lastNavigatedMethodReference = startMethod
|
||||
|
||||
private val lastNavigatedMethodInstructions get() = with(immutable()) {
|
||||
instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the method at the specified index.
|
||||
*
|
||||
* @param index The index of the method to navigate to.
|
||||
*
|
||||
* @return This [MethodNavigator].
|
||||
*/
|
||||
fun at(vararg index: Int): MethodNavigator {
|
||||
index.forEach {
|
||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the method at the specified index that matches the specified predicate.
|
||||
*
|
||||
* @param index The index of the method to navigate to.
|
||||
* @param predicate The predicate to match.
|
||||
*/
|
||||
fun at(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
|
||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
|
||||
.filter(predicate).asIterable().getMethodReferenceAt(index)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method reference at the specified index.
|
||||
*
|
||||
* @param index The index of the method reference to get.
|
||||
*/
|
||||
private fun Iterable<Instruction>.getMethodReferenceAt(index: Int): MethodReference {
|
||||
val instruction = elementAt(index) as? ReferenceInstruction
|
||||
?: throw NavigateException("Instruction at index $index is not a method reference.")
|
||||
|
||||
return instruction.reference as MethodReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last navigated method mutably.
|
||||
*
|
||||
* @return The last navigated method mutably.
|
||||
*/
|
||||
fun mutable() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
|
||||
as MutableMethod
|
||||
|
||||
/**
|
||||
* Get the last navigated method immutably.
|
||||
*
|
||||
* @return The last navigated method immutably.
|
||||
*/
|
||||
fun immutable() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
|
||||
|
||||
/**
|
||||
* Predicate to match the class defining the current method reference.
|
||||
*/
|
||||
private val matchesCurrentMethodReferenceDefiningClass = { classDef: ClassDef ->
|
||||
classDef.type == lastNavigatedMethodReference.definingClass
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first [lastNavigatedMethodReference] in the class.
|
||||
*/
|
||||
private val ClassDef.firstMethodBySignature get() = methods.first {
|
||||
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception thrown when navigating fails.
|
||||
*
|
||||
* @param message The message of the exception.
|
||||
*/
|
||||
internal class NavigateException internal constructor(message: String) : Exception(message)
|
||||
}
|
||||
@@ -4,23 +4,18 @@ import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
|
||||
/**
|
||||
* A class that represents a set of classes and proxies.
|
||||
* A list of classes and proxies.
|
||||
*
|
||||
* @param classes The classes to be backed by proxies.
|
||||
*/
|
||||
class ProxyClassList internal constructor(classes: MutableSet<ClassDef>) : MutableSet<ClassDef> by classes {
|
||||
internal val proxies = mutableListOf<ClassProxy>()
|
||||
|
||||
/**
|
||||
* Add a [ClassProxy].
|
||||
*/
|
||||
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
|
||||
class ProxyClassList internal constructor(classes: MutableList<ClassDef>) : MutableList<ClassDef> by classes {
|
||||
internal val proxyPool = mutableListOf<ClassProxy>()
|
||||
|
||||
/**
|
||||
* Replace all classes with their mutated versions.
|
||||
*/
|
||||
internal fun replaceClasses() =
|
||||
proxies.removeIf { proxy ->
|
||||
proxyPool.removeIf { proxy ->
|
||||
// If the proxy is unused, return false to keep it in the proxies list.
|
||||
if (!proxy.resolved) return@removeIf false
|
||||
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package app.revanced.patcher.util.method
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
/**
|
||||
* Find a method from another method via instruction offsets.
|
||||
* @param bytecodeContext The context to use when resolving the next method reference.
|
||||
* @param currentMethod The method to start from.
|
||||
*/
|
||||
class MethodWalker internal constructor(
|
||||
private val bytecodeContext: BytecodeContext,
|
||||
private var currentMethod: Method,
|
||||
) {
|
||||
/**
|
||||
* Get the method which was walked last.
|
||||
*
|
||||
* It is possible to cast this method to a [MutableMethod], if the method has been walked mutably.
|
||||
*
|
||||
* @return The method which was walked last.
|
||||
*/
|
||||
fun getMethod(): Method {
|
||||
return currentMethod
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk to a method defined at the offset in the instruction list of the current method.
|
||||
*
|
||||
* The current method will be mutable.
|
||||
*
|
||||
* @param offset The offset of the instruction. This instruction must be of format 35c.
|
||||
* @param walkMutable If this is true, the class of the method will be resolved mutably.
|
||||
* @return The same [MethodWalker] instance with the method at [offset].
|
||||
*/
|
||||
fun nextMethod(
|
||||
offset: Int,
|
||||
walkMutable: Boolean = false,
|
||||
): MethodWalker {
|
||||
currentMethod.implementation?.instructions?.let { instructions ->
|
||||
val instruction = instructions.elementAt(offset)
|
||||
|
||||
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
|
||||
val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
|
||||
|
||||
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
|
||||
currentMethod =
|
||||
methods.first {
|
||||
return@first MethodUtil.methodSignaturesMatch(it, newMethod)
|
||||
}
|
||||
return this
|
||||
}
|
||||
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")
|
||||
}
|
||||
|
||||
internal class MethodNotFoundException(exception: String) : Exception(exception)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
*
|
||||
* A class proxy simply holds a reference to the original class
|
||||
* and allocates a mutable clone for the original class if needed.
|
||||
*
|
||||
* @param immutableClass The class to proxy.
|
||||
*/
|
||||
class ClassProxy internal constructor(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package app.revanced.patcher.util.smali
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcodes
|
||||
@@ -13,7 +14,7 @@ import org.antlr.runtime.CommonTokenStream
|
||||
import org.antlr.runtime.TokenSource
|
||||
import org.antlr.runtime.tree.CommonTreeNodeStream
|
||||
import java.io.InputStreamReader
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
private const val METHOD_TEMPLATE = """
|
||||
.class LInlineCompiler;
|
||||
@@ -64,7 +65,7 @@ class InlineSmaliCompiler {
|
||||
val dexGen = smaliTreeWalker(treeStream)
|
||||
dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault()))
|
||||
val classDef = dexGen.smali_file()
|
||||
return classDef.methods.first().implementation!!.instructions.map { it as BuilderInstruction }
|
||||
return classDef.methods.first().instructions.map { it as BuilderInstruction }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
236
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
236
src/test/kotlin/app/revanced/patcher/PatcherTest.kt
Normal file
@@ -0,0 +1,236 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.*
|
||||
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
|
||||
import app.revanced.patcher.util.ProxyClassList
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import java.util.logging.Logger
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal object PatcherTest {
|
||||
private lateinit var patcher: Patcher
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
patcher = mockk<Patcher> {
|
||||
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
|
||||
setPrivateField(
|
||||
"config",
|
||||
mockk<PatcherConfig> {
|
||||
every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE
|
||||
},
|
||||
)
|
||||
setPrivateField(
|
||||
"logger",
|
||||
Logger.getAnonymousLogger(),
|
||||
)
|
||||
|
||||
every { context.bytecodeContext.classes } returns mockk(relaxed = true)
|
||||
every { this@mockk() } answers { callOriginal() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executes patches in correct order`() {
|
||||
val executed = mutableListOf<String>()
|
||||
|
||||
val patches = setOf(
|
||||
bytecodePatch { execute { executed += "1" } },
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
execute { executed += "2" }
|
||||
finalize { executed += "-2" }
|
||||
},
|
||||
bytecodePatch { execute { executed += "3" } },
|
||||
)
|
||||
|
||||
execute { executed += "4" }
|
||||
finalize { executed += "-1" }
|
||||
},
|
||||
)
|
||||
|
||||
assert(executed.isEmpty())
|
||||
|
||||
patches()
|
||||
|
||||
assertEquals(
|
||||
listOf("1", "2", "3", "4", "-1", "-2"),
|
||||
executed,
|
||||
"Expected patches to be executed in correct order.",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handles execution of patches correctly when exceptions occur`() {
|
||||
val executed = mutableListOf<String>()
|
||||
|
||||
infix fun Patch<*>.produces(equals: List<String>) {
|
||||
val patches = setOf(this)
|
||||
|
||||
patches()
|
||||
|
||||
assertEquals(equals, executed, "Expected patches to be executed in correct order.")
|
||||
|
||||
executed.clear()
|
||||
}
|
||||
|
||||
// No patches execute successfully,
|
||||
// because the dependency patch throws an exception inside the execute block.
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
execute { throw PatchException("1") }
|
||||
finalize { executed += "-2" }
|
||||
},
|
||||
)
|
||||
|
||||
execute { executed += "2" }
|
||||
finalize { executed += "-1" }
|
||||
} produces emptyList()
|
||||
|
||||
// The dependency patch is executed successfully,
|
||||
// because only the dependant patch throws an exception inside the finalize block.
|
||||
// Patches that depend on a failed patch should not be executed,
|
||||
// but patches that are depended on by a failed patch should be executed.
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
execute { executed += "1" }
|
||||
finalize { executed += "-2" }
|
||||
},
|
||||
)
|
||||
|
||||
execute { throw PatchException("2") }
|
||||
finalize { executed += "-1" }
|
||||
} produces listOf("1", "-2")
|
||||
|
||||
// Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch,
|
||||
// the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block.
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
execute { executed += "1" }
|
||||
finalize { throw PatchException("-2") }
|
||||
},
|
||||
)
|
||||
|
||||
execute { executed += "2" }
|
||||
finalize { executed += "-1" }
|
||||
} produces listOf("1", "2", "-1")
|
||||
|
||||
// The dependency patch is executed successfully,
|
||||
// because the dependant patch raises an exception in the finalize block.
|
||||
// Patches that depend on a failed patch should not be executed,
|
||||
// but patches that are depended on by a failed patch should be executed.
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
execute { executed += "1" }
|
||||
finalize { executed += "-2" }
|
||||
},
|
||||
)
|
||||
|
||||
execute { executed += "2" }
|
||||
finalize { throw PatchException("-1") }
|
||||
} produces listOf("1", "2", "-2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws if unmatched fingerprint match is delegated`() {
|
||||
val patch = bytecodePatch {
|
||||
// Fingerprint can never match.
|
||||
val match by fingerprint { }
|
||||
// Manually add the fingerprint.
|
||||
app.revanced.patcher.fingerprint { }()
|
||||
|
||||
execute {
|
||||
// Throws, because the fingerprint can't be matched.
|
||||
match.patternMatch
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(2, patch.fingerprints.size)
|
||||
|
||||
assertTrue(
|
||||
patch().exception != null,
|
||||
"Expected an exception because the fingerprint can't match.",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `matches fingerprint`() {
|
||||
mockClassWithMethod()
|
||||
|
||||
val patches = setOf(bytecodePatch { fingerprint { this returns "V" } })
|
||||
|
||||
assertNull(
|
||||
patches.first().fingerprints.first().match,
|
||||
"Expected fingerprint to be matched before execution.",
|
||||
)
|
||||
|
||||
patches()
|
||||
|
||||
assertDoesNotThrow("Expected fingerprint to be matched.") {
|
||||
assertEquals(
|
||||
"V",
|
||||
patches.first().fingerprints.first().match!!.method.returnType,
|
||||
"Expected fingerprint to be matched.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
|
||||
every { patcher.context.executablePatches } returns toMutableSet()
|
||||
|
||||
return runBlocking { patcher().toList() }
|
||||
}
|
||||
|
||||
private operator fun Patch<*>.invoke() = setOf(this)().first()
|
||||
|
||||
private fun Any.setPrivateField(field: String, value: Any) {
|
||||
this::class.java.getDeclaredField(field).apply {
|
||||
this.isAccessible = true
|
||||
set(this@setPrivateField, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockClassWithMethod() {
|
||||
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
|
||||
mutableListOf(
|
||||
ImmutableClassDef(
|
||||
"class",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
listOf(
|
||||
ImmutableMethod(
|
||||
"class",
|
||||
"method",
|
||||
emptyList(),
|
||||
"V",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
private object AnnotationExtensionsTest {
|
||||
@Test
|
||||
fun `find annotation in annotated class`() {
|
||||
assertNotNull(TestClasses.Annotation2::class.findAnnotationRecursively(TestClasses.Annotation::class))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find annotation`() {
|
||||
assertNotNull(TestClasses.AnnotatedClass::class.findAnnotationRecursively(TestClasses.Annotation::class))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find annotation recursively in super class`() {
|
||||
assertNotNull(TestClasses.AnnotatedClass2::class.findAnnotationRecursively(TestClasses.Annotation::class))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find annotation recursively in super class with annotation`() {
|
||||
assertNotNull(TestClasses.AnnotatedTestClass3::class.findAnnotationRecursively(TestClasses.Annotation::class))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `don't find unknown annotation in annotated class`() {
|
||||
assertNull(TestClasses.AnnotatedClass::class.findAnnotationRecursively(TestClasses.UnknownAnnotation::class))
|
||||
}
|
||||
|
||||
object TestClasses {
|
||||
annotation class Annotation
|
||||
|
||||
@Annotation
|
||||
annotation class Annotation2
|
||||
|
||||
annotation class UnknownAnnotation
|
||||
|
||||
@Annotation
|
||||
abstract class AnnotatedClass
|
||||
|
||||
@Annotation2
|
||||
class AnnotatedTestClass3
|
||||
|
||||
abstract class AnnotatedClass2 : AnnotatedClass()
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import kotlin.test.Test
|
||||
import app.revanced.patcher.patch.annotation.Patch as PatchAnnotation
|
||||
|
||||
object PatchInitializationTest {
|
||||
@Test
|
||||
fun `initialize using constructor`() {
|
||||
val patch =
|
||||
object : RawResourcePatch(name = "Resource patch test") {
|
||||
override fun execute(context: ResourceContext) {}
|
||||
}
|
||||
|
||||
assert(patch.name == "Resource patch test")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initialize using annotation`() {
|
||||
val patch =
|
||||
@PatchAnnotation("Resource patch test")
|
||||
object : RawResourcePatch() {
|
||||
override fun execute(context: ResourceContext) {}
|
||||
}
|
||||
|
||||
assert(patch.name == "Resource patch test")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.full.companionObject
|
||||
import kotlin.reflect.full.declaredFunctions
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// region Test patches.
|
||||
|
||||
// Not loaded, because it's unnamed.
|
||||
val publicUnnamedPatch = bytecodePatch {
|
||||
}
|
||||
|
||||
// Loaded, because it's named.
|
||||
val publicPatch = bytecodePatch("Public") {
|
||||
}
|
||||
|
||||
// Not loaded, because it's private.
|
||||
private val privateUnnamedPatch = bytecodePatch {
|
||||
}
|
||||
|
||||
// Not loaded, because it's private.
|
||||
private val privatePatch = bytecodePatch("Private") {
|
||||
}
|
||||
|
||||
// Not loaded, because it's unnamed.
|
||||
fun publicUnnamedPatchFunction() = publicUnnamedPatch
|
||||
|
||||
// Loaded, because it's named.
|
||||
fun publicNamedPatchFunction() = bytecodePatch("Public") { }
|
||||
|
||||
// Not loaded, because it's parameterized.
|
||||
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) = publicNamedPatchFunction()
|
||||
|
||||
// Not loaded, because it's private.
|
||||
private fun privateUnnamedPatchFunction() = privateUnnamedPatch
|
||||
|
||||
// Not loaded, because it's private.
|
||||
private fun privateNamedPatchFunction() = privatePatch
|
||||
|
||||
// endregion
|
||||
|
||||
internal object PatchLoaderTest {
|
||||
private const val LOAD_PATCHES_FUNCTION_NAME = "loadPatches"
|
||||
private val TEST_PATCHES_CLASS = ::publicPatch.javaField!!.declaringClass.name
|
||||
private val TEST_PATCHES_CLASS_LOADER = ::publicPatch.javaClass.classLoader
|
||||
|
||||
@Test
|
||||
fun `loads patches correctly`() {
|
||||
// Get instance of private PatchLoader.Companion class.
|
||||
val patchLoaderCompanionObject = getPrivateFieldByType(
|
||||
PatchLoader::class.java,
|
||||
PatchLoader::class.companionObject!!.javaObjectType,
|
||||
)
|
||||
|
||||
// Get private PatchLoader.Companion.loadPatches function from PatchLoader.Companion.
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val loadPatchesFunction = getPrivateFunctionByName(
|
||||
patchLoaderCompanionObject,
|
||||
LOAD_PATCHES_FUNCTION_NAME,
|
||||
) as KFunction<Map<File, Set<Patch<*>>>>
|
||||
|
||||
// Call private PatchLoader.Companion.loadPatches function.
|
||||
val patches = loadPatchesFunction.call(
|
||||
patchLoaderCompanionObject,
|
||||
TEST_PATCHES_CLASS_LOADER,
|
||||
mapOf(File("patchesFile") to setOf(TEST_PATCHES_CLASS)),
|
||||
).values.first()
|
||||
|
||||
assertEquals(
|
||||
2,
|
||||
patches.size,
|
||||
"Expected 2 patches to be loaded, " +
|
||||
"because there's only two named patches declared as public static fields " +
|
||||
"or returned by public static and non-parametrized methods.",
|
||||
)
|
||||
}
|
||||
|
||||
private fun getPrivateFieldByType(cls: Class<*>, fieldType: Class<*>) =
|
||||
cls.declaredFields.first { it.type == fieldType }.apply { isAccessible = true }.get(null)
|
||||
|
||||
private fun getPrivateFunctionByName(obj: Any, @Suppress("SameParameterValue") methodName: String) =
|
||||
obj::class.declaredFunctions.first { it.name == methodName }.apply { isAccessible = true }
|
||||
}
|
||||
67
src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt
Normal file
67
src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal object PatchTest {
|
||||
@Test
|
||||
fun `can create patch with name`() {
|
||||
val patch = bytecodePatch(name = "Test") {}
|
||||
|
||||
assertEquals("Test", patch.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can create patch with compatible packages`() {
|
||||
val patch = bytecodePatch(name = "Test") {
|
||||
compatibleWith(
|
||||
"compatible.package"("1.0.0"),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(1, patch.compatiblePackages!!.size)
|
||||
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can create patch with fingerprints`() {
|
||||
val externalFingerprint = fingerprint {}
|
||||
|
||||
val patch = bytecodePatch(name = "Test") {
|
||||
val externalFingerprintMatch by externalFingerprint()
|
||||
val internalFingerprintMatch by fingerprint {}
|
||||
|
||||
execute {
|
||||
externalFingerprintMatch.method
|
||||
internalFingerprintMatch.method
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(2, patch.fingerprints.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can create patch with dependencies`() {
|
||||
val patch = bytecodePatch(name = "Test") {
|
||||
dependsOn(resourcePatch {})
|
||||
}
|
||||
|
||||
assertEquals(1, patch.dependencies.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can create patch with options`() {
|
||||
val patch = bytecodePatch(name = "Test") {
|
||||
val print by stringOption("print")
|
||||
val custom = option<String>("custom")()
|
||||
|
||||
execute {
|
||||
println(print)
|
||||
println(custom.value)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(2, patch.options.size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package app.revanced.patcher.patch.options
|
||||
|
||||
import app.revanced.patcher.patch.*
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.reflect.typeOf
|
||||
import kotlin.test.*
|
||||
|
||||
internal object OptionsTest {
|
||||
private val optionsTestPatch = bytecodePatch {
|
||||
booleanOption("bool", true)
|
||||
|
||||
stringOption("required", "default", required = true)
|
||||
|
||||
stringsOption("list", listOf("1", "2"))
|
||||
|
||||
stringOption("choices", "value", values = mapOf("Valid option value" to "valid"))
|
||||
|
||||
stringOption("validated", "default") { it == "valid" }
|
||||
|
||||
stringOption("resettable", null, required = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not fail because default value is unvalidated`() = options {
|
||||
assertDoesNotThrow { get("required") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not allow setting custom value with validation`() = options {
|
||||
// Getter validation on incorrect value.
|
||||
assertThrows<OptionException.ValueValidationException> {
|
||||
set("validated", get("validated"))
|
||||
}
|
||||
|
||||
// Setter validation on incorrect value.
|
||||
assertThrows<OptionException.ValueValidationException> {
|
||||
set("validated", "invalid")
|
||||
}
|
||||
|
||||
// Setter validation on correct value.
|
||||
assertDoesNotThrow {
|
||||
set("validated", "valid")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw due to incorrect type`() = options {
|
||||
assertThrows<OptionException.InvalidValueTypeException> {
|
||||
set("bool", "not a boolean")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should be nullable`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("bool", null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option should not be found`() = options {
|
||||
assertThrows<OptionException.OptionNotFoundException> {
|
||||
set("this option does not exist", 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should be able to add options manually`() = options {
|
||||
assertDoesNotThrow {
|
||||
bytecodePatch {
|
||||
get("list")()
|
||||
}.options["list"]
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow setting value from values`() = options {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val option = get("choices") as Option<String>
|
||||
|
||||
option.value = option.values!!.values.last()
|
||||
|
||||
assertTrue(option.value == "valid")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow setting custom value`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("choices", "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow resetting value`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("choices", null)
|
||||
}
|
||||
|
||||
assert(get("choices").value == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset should not fail`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("resettable", "test")
|
||||
get("resettable").reset()
|
||||
}
|
||||
|
||||
assertThrows<OptionException.ValueRequiredException> {
|
||||
get("resettable").value
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option types should be known`() = options {
|
||||
assertEquals(typeOf<List<String>>(), get("list").type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getting default value should work`() = options {
|
||||
assertDoesNotThrow {
|
||||
assertNull(get("resettable").default)
|
||||
}
|
||||
}
|
||||
|
||||
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package app.revanced.patcher.patch.options
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.patch.BytecodePatch
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringArrayPatchOption
|
||||
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class PatchOptionsTest {
|
||||
@Test
|
||||
fun `should not fail because default value is unvalidated`() {
|
||||
assertDoesNotThrow { OptionsTestPatch.requiredStringOption }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not allow setting custom value with validation`() {
|
||||
// Getter validation on incorrect value.
|
||||
assertThrows<PatchOptionException.ValueValidationException> { OptionsTestPatch.validatedOption }
|
||||
|
||||
// Setter validation on incorrect value.
|
||||
assertThrows<PatchOptionException.ValueValidationException> { OptionsTestPatch.validatedOption = "invalid" }
|
||||
|
||||
// Setter validation on correct value.
|
||||
assertDoesNotThrow { OptionsTestPatch.validatedOption = "valid" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw due to incorrect type`() {
|
||||
assertThrows<PatchOptionException.InvalidValueTypeException> {
|
||||
OptionsTestPatch.options["bool"] = "not a boolean"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should be nullable`() {
|
||||
OptionsTestPatch.booleanOption = null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option should not be found`() {
|
||||
assertThrows<PatchOptionException.PatchOptionNotFoundException> {
|
||||
OptionsTestPatch.options["this option does not exist"] = 1
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should be able to add options manually`() {
|
||||
assertThrows<PatchOptionException.InvalidValueTypeException> {
|
||||
OptionsTestPatch.options["array"] = OptionsTestPatch.stringArrayOption
|
||||
}
|
||||
assertDoesNotThrow {
|
||||
OptionsTestPatch.options.register(OptionsTestPatch.stringArrayOption)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Test
|
||||
fun `should allow setting value from values`() =
|
||||
with(OptionsTestPatch.options["choices"] as PatchOption<String>) {
|
||||
value = values!!.values.last()
|
||||
assertTrue(value == "valid")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow setting custom value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = "unknown" }
|
||||
|
||||
@Test
|
||||
fun `should allow resetting value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = null }
|
||||
|
||||
@Test
|
||||
fun `reset should not fail`() {
|
||||
assertDoesNotThrow {
|
||||
OptionsTestPatch.resettableOption.value = "test"
|
||||
OptionsTestPatch.resettableOption.reset()
|
||||
}
|
||||
|
||||
assertThrows<PatchOptionException.ValueRequiredException> {
|
||||
OptionsTestPatch.resettableOption.value
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option types should be known`() = assertTrue(OptionsTestPatch.options["array"].valueType == "StringArray")
|
||||
|
||||
@Test
|
||||
fun `getting default value should work`() = assertDoesNotThrow { assertNull(OptionsTestPatch.resettableOption.default) }
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private object OptionsTestPatch : BytecodePatch() {
|
||||
var booleanOption by booleanPatchOption(
|
||||
"bool",
|
||||
true,
|
||||
)
|
||||
var requiredStringOption by stringPatchOption(
|
||||
"required",
|
||||
"default",
|
||||
required = true,
|
||||
)
|
||||
var stringArrayOption =
|
||||
stringArrayPatchOption(
|
||||
"array",
|
||||
arrayOf("1", "2"),
|
||||
)
|
||||
var stringOptionWithChoices by stringPatchOption(
|
||||
"choices",
|
||||
"value",
|
||||
values = mapOf("Valid option value" to "valid"),
|
||||
)
|
||||
var validatedOption by stringPatchOption(
|
||||
"validated",
|
||||
"default",
|
||||
) { it == "valid" }
|
||||
var resettableOption =
|
||||
stringPatchOption(
|
||||
"resettable",
|
||||
null,
|
||||
required = true,
|
||||
)
|
||||
|
||||
override fun execute(context: BytecodeContext) {}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package app.revanced.patcher.patch.usage
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.extensions.or
|
||||
import app.revanced.patcher.patch.BytecodePatch
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.annotation.CompatiblePackage
|
||||
import app.revanced.patcher.patch.annotation.Patch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Format
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x
|
||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableField
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReference
|
||||
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
|
||||
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
||||
import com.android.tools.smali.dexlib2.util.Preconditions
|
||||
import com.google.common.collect.ImmutableList
|
||||
|
||||
@Suppress("unused")
|
||||
@Patch(
|
||||
name = "Example bytecode patch",
|
||||
description = "Example demonstration of a bytecode patch.",
|
||||
dependencies = [ExampleResourcePatch::class],
|
||||
compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))],
|
||||
)
|
||||
object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
|
||||
// Entry point of a patch. Supplied fingerprints are resolved at this point.
|
||||
override fun execute(context: BytecodeContext) {
|
||||
ExampleFingerprint.result?.let { result ->
|
||||
// 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.scanResult.patternScanResult!!.startIndex
|
||||
|
||||
result.mutableMethod.apply {
|
||||
replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
|
||||
|
||||
// Store the fields initial value into the first virtual register.
|
||||
replaceInstruction(0, "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;")
|
||||
|
||||
// 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.
|
||||
addInstructionsWithLabels(
|
||||
startIndex + 2,
|
||||
"""
|
||||
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
|
||||
move-result-object v1
|
||||
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
// Find the class in which the method matching our fingerprint is defined in.
|
||||
context.findClass(result.classDef.type)!!.mutableClass.apply {
|
||||
// Add a new method that returns a string.
|
||||
methods.add(
|
||||
ImmutableMethod(
|
||||
result.classDef.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.
|
||||
fields.add(
|
||||
ImmutableField(
|
||||
type,
|
||||
"dummyField",
|
||||
"Ljava/io/PrintStream;",
|
||||
AccessFlags.PRIVATE or AccessFlags.STATIC,
|
||||
ImmutableFieldEncodedValue(
|
||||
ImmutableFieldReference(
|
||||
"Ljava/lang/System;",
|
||||
"out",
|
||||
"Ljava/io/PrintStream;",
|
||||
),
|
||||
),
|
||||
null,
|
||||
null,
|
||||
).toMutable(),
|
||||
)
|
||||
}
|
||||
} ?: throw PatchException("Fingerprint failed to resolve.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an existing instruction with a new one containing a reference to a new string.
|
||||
* @param index The index of the instruction to replace.
|
||||
* @param string The replacement string.
|
||||
*/
|
||||
private fun MutableMethod.replaceStringAt(
|
||||
index: Int,
|
||||
string: String,
|
||||
) {
|
||||
val instruction = getInstruction(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.
|
||||
replaceInstruction(
|
||||
index,
|
||||
"const-string ${strInstruction.registerA}, ${ImmutableStringReference(string)}",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package app.revanced.patcher.patch.usage
|
||||
import app.revanced.patcher.extensions.or
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
@FuzzyPatternScanMethod(2)
|
||||
object ExampleFingerprint : MethodFingerprint(
|
||||
"V",
|
||||
AccessFlags.PUBLIC or AccessFlags.STATIC,
|
||||
listOf("[L"),
|
||||
listOf(
|
||||
Opcode.SGET_OBJECT,
|
||||
null, // Matching unknown opcodes.
|
||||
Opcode.INVOKE_STATIC, // This is intentionally wrong to test fuzzy matching.
|
||||
Opcode.RETURN_VOID,
|
||||
),
|
||||
null,
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package app.revanced.patcher.patch.usage
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.ResourcePatch
|
||||
import org.w3c.dom.Element
|
||||
|
||||
class ExampleResourcePatch : ResourcePatch() {
|
||||
override fun execute(context: ResourceContext) {
|
||||
context.document["AndroidManifest.xml"].use { document ->
|
||||
val element = document.getElementsByTagName("application").item(0) as Element
|
||||
element.setAttribute("exampleAttribute", "exampleValue")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,23 +18,24 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class InlineSmaliCompilerTest {
|
||||
internal object InlineSmaliCompilerTest {
|
||||
@Test
|
||||
fun `compiler should output valid instruction`() {
|
||||
fun `outputs valid instruction`() {
|
||||
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
|
||||
val have = "const-string v0, \"Test\"".toInstruction()
|
||||
instructionEquals(want, have)
|
||||
|
||||
assertInstructionsEqual(want, have)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compiler should support branching with own branches`() {
|
||||
fun `supports branching with own branches`() {
|
||||
val method = createMethod()
|
||||
val insnAmount = 8
|
||||
val insnIndex = insnAmount - 2
|
||||
val targetIndex = insnIndex - 1
|
||||
val instructionCount = 8
|
||||
val instructionIndex = instructionCount - 2
|
||||
val targetIndex = instructionIndex - 1
|
||||
|
||||
method.addInstructions(
|
||||
arrayOfNulls<String>(insnAmount).also {
|
||||
arrayOfNulls<String>(instructionCount).also {
|
||||
Arrays.fill(it, "const/4 v0, 0x0")
|
||||
}.joinToString("\n"),
|
||||
)
|
||||
@@ -47,14 +48,15 @@ internal class InlineSmaliCompilerTest {
|
||||
""",
|
||||
)
|
||||
|
||||
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
|
||||
assertEquals(targetIndex, insn.target.location.index)
|
||||
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
|
||||
|
||||
assertEquals(targetIndex, instruction.target.location.index)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `compiler should support branching to outside branches`() {
|
||||
fun `supports branching to outside branches`() {
|
||||
val method = createMethod()
|
||||
val insnIndex = 3
|
||||
val instructionIndex = 3
|
||||
val labelIndex = 1
|
||||
|
||||
method.addInstructions(
|
||||
@@ -76,35 +78,30 @@ internal class InlineSmaliCompilerTest {
|
||||
ExternalLabel("test", method.getInstruction(1)),
|
||||
)
|
||||
|
||||
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex)
|
||||
assertTrue(insn.target.isPlaced, "Label was not placed")
|
||||
assertEquals(labelIndex, insn.target.location.index)
|
||||
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
|
||||
assertTrue(instruction.target.isPlaced, "Label was not placed")
|
||||
assertEquals(labelIndex, instruction.target.location.index)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun createMethod(
|
||||
name: String = "dummy",
|
||||
returnType: String = "V",
|
||||
accessFlags: Int = AccessFlags.STATIC.value,
|
||||
registerCount: Int = 1,
|
||||
) = ImmutableMethod(
|
||||
"Ldummy;",
|
||||
name,
|
||||
emptyList(), // parameters
|
||||
returnType,
|
||||
accessFlags,
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
MutableMethodImplementation(registerCount),
|
||||
).toMutable()
|
||||
private fun createMethod(
|
||||
name: String = "dummy",
|
||||
returnType: String = "V",
|
||||
accessFlags: Int = AccessFlags.STATIC.value,
|
||||
registerCount: Int = 1,
|
||||
) = ImmutableMethod(
|
||||
"Ldummy;",
|
||||
name,
|
||||
emptyList(), // parameters
|
||||
returnType,
|
||||
accessFlags,
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
MutableMethodImplementation(registerCount),
|
||||
).toMutable()
|
||||
|
||||
private fun instructionEquals(
|
||||
want: BuilderInstruction,
|
||||
have: BuilderInstruction,
|
||||
) {
|
||||
assertEquals(want.opcode, have.opcode)
|
||||
assertEquals(want.format, have.format)
|
||||
assertEquals(want.codeUnits, have.codeUnits)
|
||||
}
|
||||
private fun assertInstructionsEqual(want: BuilderInstruction, have: BuilderInstruction) {
|
||||
assertEquals(want.opcode, have.opcode)
|
||||
assertEquals(want.format, have.format)
|
||||
assertEquals(want.codeUnits, have.codeUnits)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user