mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-11 13:56:16 +00:00
feat: Builds now
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,7 @@ kotlin {
|
|||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget.set(JvmTarget.JVM_11)
|
jvmTarget.set(JvmTarget.JVM_11)
|
||||||
|
|
||||||
freeCompilerArgs = listOf("-Xcontext-receivers")
|
freeCompilerArgs = listOf("-Xcontext-parameters")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
org.gradle.caching = true
|
org.gradle.caching = true
|
||||||
version = 21.1.0-dev.5
|
version = 22.0.0
|
||||||
|
|||||||
909
src/main/kotlin/app/revanced/patcher/Fingerprint.kt
Normal file
909
src/main/kotlin/app/revanced/patcher/Fingerprint.kt
Normal file
@@ -0,0 +1,909 @@
|
|||||||
|
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.InstructionLocation.*
|
||||||
|
import app.revanced.patcher.Match.PatternMatch
|
||||||
|
import app.revanced.patcher.Matcher.MatchContext
|
||||||
|
import app.revanced.patcher.extensions.*
|
||||||
|
import app.revanced.patcher.extensions.opcode
|
||||||
|
import app.revanced.patcher.extensions.string
|
||||||
|
import app.revanced.patcher.patch.BytecodePatchContext
|
||||||
|
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
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
|
||||||
|
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fingerprint for a method. A fingerprint is a partial description of a method.
|
||||||
|
* It is used to uniquely match a method by its characteristics.
|
||||||
|
*
|
||||||
|
* An example fingerprint for a public method that takes a single string parameter and returns void:
|
||||||
|
* ```
|
||||||
|
* fingerprint {
|
||||||
|
* accessFlags(AccessFlags.PUBLIC)
|
||||||
|
* returns("V")
|
||||||
|
* parameters("Ljava/lang/String;")
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @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 filters A list of filters to match, declared in the same order the instructions appear in the method.
|
||||||
|
* @param strings A list of the strings that appear anywhere in the method. Compared using [String.contains].
|
||||||
|
* @param custom A custom condition for this fingerprint.
|
||||||
|
*/
|
||||||
|
class Fingerprint internal constructor(
|
||||||
|
internal val accessFlags: Int?,
|
||||||
|
internal val returnType: String?,
|
||||||
|
internal val parameters: List<String>?,
|
||||||
|
internal val filters: List<InstructionFilter>?,
|
||||||
|
@Deprecated("Instead use instruction filters")
|
||||||
|
internal val strings: List<String>?,
|
||||||
|
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
|
||||||
|
) {
|
||||||
|
@Suppress("ktlint:standard:backing-property-naming")
|
||||||
|
// Backing field needed for lazy initialization.
|
||||||
|
private var _matchOrNull: Match? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the current match, forcing this fingerprint to resolve again.
|
||||||
|
* This method should only be used if this fingerprint is re-used after it's modified,
|
||||||
|
* and the prior match indexes are no longer correct.
|
||||||
|
*/
|
||||||
|
fun clearMatch() {
|
||||||
|
_matchOrNull = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match for this [Fingerprint], or `null` if no matches exist.
|
||||||
|
*/
|
||||||
|
context(context: BytecodePatchContext)
|
||||||
|
fun matchOrNull(): Match? {
|
||||||
|
if (_matchOrNull != null) return _matchOrNull
|
||||||
|
|
||||||
|
val matchStrings = indexedMatcher<Instruction> {
|
||||||
|
strings?.forEach { add { string { contains(it) } } }
|
||||||
|
}
|
||||||
|
|
||||||
|
val matchIndices = indexedMatcher<Instruction>()
|
||||||
|
|
||||||
|
val match = context(_: MatchContext) fun Method.(): Boolean {
|
||||||
|
if (this@Fingerprint.accessFlags != null && this@Fingerprint.accessFlags != accessFlags)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (this@Fingerprint.returnType != null && this@Fingerprint.returnType != returnType)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (this@Fingerprint.parameters != null && parametersStartsWith(
|
||||||
|
this@Fingerprint.parameters,
|
||||||
|
parameters
|
||||||
|
)
|
||||||
|
) return false
|
||||||
|
|
||||||
|
if (custom != null && !custom.invoke(this, context.lookupMaps.classDefsByType[definingClass]!!))
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
if (strings != null && !matchStrings(instructionsOrNull ?: return false))
|
||||||
|
return false
|
||||||
|
|
||||||
|
fun InstructionFilter.evaluate(instruction: Instruction): Boolean {
|
||||||
|
return when (this) {
|
||||||
|
is AnyInstruction -> filters.any { evaluate(instruction) }
|
||||||
|
is CheckCastFilter -> {
|
||||||
|
val type = type()
|
||||||
|
|
||||||
|
instruction.opcode(Opcode.CHECK_CAST) &&
|
||||||
|
instruction.reference<TypeReference> { endsWith(type) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is FieldAccessFilter -> {
|
||||||
|
val reference = instruction.fieldReference ?: return false
|
||||||
|
|
||||||
|
if (name != null && reference.name != name()) return false
|
||||||
|
|
||||||
|
if (type != null && !reference.type.startsWith(type())) return false
|
||||||
|
|
||||||
|
if (definingClass != null) {
|
||||||
|
val definingClass = definingClass()
|
||||||
|
|
||||||
|
if (!reference.definingClass.endsWith(definingClass))
|
||||||
|
// else, the method call is for 'this' class.
|
||||||
|
if (!(definingClass == "this" && reference.definingClass == method.definingClass)) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
is LiteralFilter -> {
|
||||||
|
instruction.wideLiteral?.equals(literal()) ?: return false
|
||||||
|
|
||||||
|
opcodes != null && !opcodes.contains(instruction.opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MethodCallFilter -> {
|
||||||
|
val reference = instruction.methodReference ?: return false
|
||||||
|
|
||||||
|
if (name != null && reference.name != name()) return false
|
||||||
|
|
||||||
|
if (returnType != null && !reference.returnType.startsWith(returnType())) return false
|
||||||
|
|
||||||
|
if (parameters != null && !parametersStartsWith(
|
||||||
|
reference.parameterTypes, parameters()
|
||||||
|
)
|
||||||
|
) return false
|
||||||
|
|
||||||
|
if (definingClass != null) {
|
||||||
|
val definingClass = definingClass()
|
||||||
|
|
||||||
|
if (!reference.definingClass.endsWith(definingClass)) {
|
||||||
|
// Check if 'this' defining class is used.
|
||||||
|
// Would be nice if this also checked all super classes,
|
||||||
|
// but doing so requires iteratively checking all superclasses
|
||||||
|
// up to the root class since class defs are mere Strings.
|
||||||
|
if (!(definingClass == "this" && reference.definingClass == method.definingClass)) {
|
||||||
|
return false
|
||||||
|
} // else, the method call is for 'this' class.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opcodes != null && !opcodes.contains(instruction.opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
is NewInstanceFilter -> {
|
||||||
|
if (opcodes != null && !opcodes.contains(instruction.opcode)) return false
|
||||||
|
instruction.reference<TypeReference> { endsWith(type) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is OpcodeFilter -> instruction.opcode(opcode)
|
||||||
|
is StringFilter -> {
|
||||||
|
val string = instruction.stringReference?.string ?: return false
|
||||||
|
|
||||||
|
val filterString = string()
|
||||||
|
when (matchType) {
|
||||||
|
StringMatchType.EQUALS -> string == filterString
|
||||||
|
StringMatchType.CONTAINS -> string.contains(filterString)
|
||||||
|
StringMatchType.STARTS_WITH -> string.startsWith(filterString)
|
||||||
|
StringMatchType.ENDS_WITH -> string.endsWith(filterString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is OpcodesFilter -> opcodes?.contains(instruction.opcode) == true
|
||||||
|
else -> throw IllegalStateException("Unknown InstructionFilter type: ${this::class.java}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters != null && !matchIndices(instructionsOrNull ?: return false, "match") {
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (val location = filter.location) {
|
||||||
|
is MatchAfterImmediately -> after { filter.evaluate(this) }
|
||||||
|
is MatchAfterAnywhere -> add { filter.evaluate(this) }
|
||||||
|
is MatchAfterWithin -> after(atLeast = 1, atMost = location.matchDistance) {
|
||||||
|
filter.evaluate(
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MatchFirst -> first { filter.evaluate(this) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
val allStrings = buildList {
|
||||||
|
if (filters != null) addAll(
|
||||||
|
(filters.filterIsInstance<StringFilter>() + filters.filterIsInstance<AnyInstruction>().flatMap {
|
||||||
|
it.filters.filterIsInstance<StringFilter>()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}.map { it.string() } + (strings ?: emptyList())
|
||||||
|
|
||||||
|
val method = if (allStrings.isNotEmpty())
|
||||||
|
context.firstMethodOrNull(strings = allStrings.toTypedArray()) { match() } ?:
|
||||||
|
// Maybe a better way exists
|
||||||
|
context(MatchContext()) {
|
||||||
|
context.lookupMaps.methodsWithString.first { it.match() }
|
||||||
|
}
|
||||||
|
else context.firstMethodOrNull { match() } ?: return null
|
||||||
|
|
||||||
|
val instructionMatches = filters?.withIndex()?.map { (i, filter) ->
|
||||||
|
Match.InstructionMatch(filter, matchIndices.indices[i], method.getInstruction(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
val stringMatches = if (strings != null) matchStrings.indices.map { i ->
|
||||||
|
// TODO: Should we use the methods string or the fingerprints string
|
||||||
|
Match.StringMatch(method.getInstruction(i).stringReference!!.string, i)
|
||||||
|
} else null
|
||||||
|
|
||||||
|
return Match(
|
||||||
|
context,
|
||||||
|
context.lookupMaps.classDefsByType[method.definingClass]!!,
|
||||||
|
method,
|
||||||
|
instructionMatches,
|
||||||
|
stringMatches,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match using a [ClassDef].
|
||||||
|
*
|
||||||
|
* @param classDef The class to match against.
|
||||||
|
* @return The [Match] if a match was found or if the
|
||||||
|
* fingerprint is already matched to a method, null otherwise.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
fun matchOrNull(
|
||||||
|
classDef: ClassDef,
|
||||||
|
): Match? {
|
||||||
|
if (_matchOrNull != null) return _matchOrNull
|
||||||
|
|
||||||
|
for (method in classDef.methods) {
|
||||||
|
val match = matchOrNull(classDef, method)
|
||||||
|
if (match != null) {
|
||||||
|
_matchOrNull = match
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match using a [Method].
|
||||||
|
* The class is retrieved from the method.
|
||||||
|
*
|
||||||
|
* @param method The method to match against.
|
||||||
|
* @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
|
||||||
|
* otherwise `null`.
|
||||||
|
*/
|
||||||
|
context(context: BytecodePatchContext)
|
||||||
|
fun matchOrNull(
|
||||||
|
method: Method,
|
||||||
|
): Match? {
|
||||||
|
if (_matchOrNull != null) return _matchOrNull
|
||||||
|
|
||||||
|
return matchOrNull(context.lookupMaps.classDefsByType[method.definingClass]!!, method)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match using a [Method].
|
||||||
|
*
|
||||||
|
* @param method The method to match against.
|
||||||
|
* @param classDef The class the method is a member of.
|
||||||
|
* @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
|
||||||
|
* otherwise `null`.
|
||||||
|
*/
|
||||||
|
context(context: BytecodePatchContext)
|
||||||
|
fun matchOrNull(
|
||||||
|
classDef: ClassDef,
|
||||||
|
method: Method,
|
||||||
|
): Match? {
|
||||||
|
if (_matchOrNull != null) return _matchOrNull
|
||||||
|
|
||||||
|
if (returnType != null && !method.returnType.startsWith(returnType)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessFlags != null && accessFlags != method.accessFlags) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: parseParameters()
|
||||||
|
if (parameters != null && !parametersStartsWith(method.parameterTypes, parameters)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (custom != null && !custom.invoke(method, classDef)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy string declarations.
|
||||||
|
val stringMatches: List<Match.StringMatch>? = if (strings == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
buildList {
|
||||||
|
val instructions = method.instructionsOrNull ?: return null
|
||||||
|
|
||||||
|
var stringsList: MutableList<String>? = null
|
||||||
|
|
||||||
|
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
|
||||||
|
if (stringsList == null) {
|
||||||
|
stringsList = strings.toMutableList()
|
||||||
|
}
|
||||||
|
val index = stringsList.indexOfFirst(string::contains)
|
||||||
|
if (index < 0) return@forEachIndexed
|
||||||
|
|
||||||
|
add(Match.StringMatch(string, instructionIndex))
|
||||||
|
stringsList.removeAt(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringsList == null || stringsList.isNotEmpty()) return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val instructionMatches = if (filters == null) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val instructions = method.instructionsOrNull?.toList() ?: return null
|
||||||
|
|
||||||
|
fun matchFilters(): List<Match.InstructionMatch>? {
|
||||||
|
val lastMethodIndex = instructions.lastIndex
|
||||||
|
var instructionMatches: MutableList<Match.InstructionMatch>? = null
|
||||||
|
|
||||||
|
var firstInstructionIndex = 0
|
||||||
|
var lastMatchIndex = -1
|
||||||
|
|
||||||
|
firstFilterLoop@ while (true) {
|
||||||
|
// Matched index of the first filter.
|
||||||
|
var firstFilterIndex = -1
|
||||||
|
var subIndex = firstInstructionIndex
|
||||||
|
|
||||||
|
for (filterIndex in filters.indices) {
|
||||||
|
val filter = filters[filterIndex]
|
||||||
|
val location = filter.location
|
||||||
|
var instructionsMatched = false
|
||||||
|
|
||||||
|
while (subIndex <= lastMethodIndex &&
|
||||||
|
location.indexIsValidForMatching(
|
||||||
|
lastMatchIndex, subIndex
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val instruction = instructions[subIndex]
|
||||||
|
if (filter.matches(method, instruction)) {
|
||||||
|
lastMatchIndex = subIndex
|
||||||
|
|
||||||
|
if (filterIndex == 0) {
|
||||||
|
firstFilterIndex = subIndex
|
||||||
|
}
|
||||||
|
if (instructionMatches == null) {
|
||||||
|
instructionMatches = ArrayList<Match.InstructionMatch>(filters.size)
|
||||||
|
}
|
||||||
|
instructionMatches += Match.InstructionMatch(filter, subIndex, instruction)
|
||||||
|
instructionsMatched = true
|
||||||
|
subIndex++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
subIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instructionsMatched) {
|
||||||
|
if (filterIndex == 0) {
|
||||||
|
return null // First filter has no more matches to start from.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try again with the first filter, starting from
|
||||||
|
// the next possible first filter index.
|
||||||
|
firstInstructionIndex = firstFilterIndex + 1
|
||||||
|
instructionMatches?.clear()
|
||||||
|
continue@firstFilterLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All instruction filters matches.
|
||||||
|
return instructionMatches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchFilters() ?: return null
|
||||||
|
}
|
||||||
|
|
||||||
|
_matchOrNull = Match(
|
||||||
|
context,
|
||||||
|
classDef,
|
||||||
|
method,
|
||||||
|
instructionMatches,
|
||||||
|
stringMatches,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _matchOrNull
|
||||||
|
}
|
||||||
|
|
||||||
|
fun patchException() = PatchException("Failed to match the fingerprint: $this")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match for this [Fingerprint].
|
||||||
|
*
|
||||||
|
* @return The [Match] of this fingerprint.
|
||||||
|
* @throws PatchException If the [Fingerprint] failed to match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
fun match() = matchOrNull() ?: throw patchException()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match using a [ClassDef].
|
||||||
|
*
|
||||||
|
* @param classDef The class to match against.
|
||||||
|
* @return The [Match] of this fingerprint.
|
||||||
|
* @throws PatchException If the fingerprint failed to match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
fun match(
|
||||||
|
classDef: ClassDef,
|
||||||
|
) = matchOrNull(classDef) ?: throw patchException()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match using a [Method].
|
||||||
|
* The class is retrieved from the method.
|
||||||
|
*
|
||||||
|
* @param method The method to match against.
|
||||||
|
* @return The [Match] of this fingerprint.
|
||||||
|
* @throws PatchException If the fingerprint failed to match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
fun match(
|
||||||
|
method: Method,
|
||||||
|
) = matchOrNull(method) ?: throw patchException()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match using a [Method].
|
||||||
|
*
|
||||||
|
* @param method The method to match against.
|
||||||
|
* @param classDef The class the method is a member of.
|
||||||
|
* @return The [Match] of this fingerprint.
|
||||||
|
* @throws PatchException If the fingerprint failed to match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
fun match(
|
||||||
|
method: Method,
|
||||||
|
classDef: ClassDef,
|
||||||
|
) = matchOrNull(classDef, method) ?: throw patchException()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class the matching method is a member of, or null if this fingerprint did not match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val originalClassDefOrNull
|
||||||
|
get() = matchOrNull()?.originalClassDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The matching method, or null of this fingerprint did not match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val originalMethodOrNull
|
||||||
|
get() = matchOrNull()?.originalMethod
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mutable version of [originalClassDefOrNull].
|
||||||
|
*
|
||||||
|
* Accessing this property allocates a new mutable instance.
|
||||||
|
* Use [originalClassDefOrNull] if mutable access is not required.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val classDefOrNull
|
||||||
|
get() = matchOrNull()?.classDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mutable version of [originalMethodOrNull].
|
||||||
|
*
|
||||||
|
* Accessing this property allocates a new mutable instance.
|
||||||
|
* Use [originalMethodOrNull] if mutable access is not required.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val methodOrNull
|
||||||
|
get() = matchOrNull()?.method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match for the opcode pattern, or null if this fingerprint did not match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
@Deprecated("instead use instructionMatchesOrNull")
|
||||||
|
val patternMatchOrNull: PatternMatch?
|
||||||
|
get() {
|
||||||
|
val match = this.matchOrNull()
|
||||||
|
if (match == null || match.instructionMatchesOrNull == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return match.patternMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match for the instruction filters, or null if this fingerprint did not match.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val instructionMatchesOrNull
|
||||||
|
get() = matchOrNull()?.instructionMatchesOrNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The matches for the strings, or null if this fingerprint did not match.
|
||||||
|
*
|
||||||
|
* This does not give matches for strings declared using [string] instruction filters.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
@Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
|
||||||
|
val stringMatchesOrNull
|
||||||
|
get() = matchOrNull()?.stringMatchesOrNull
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The class the matching method is a member of.
|
||||||
|
*
|
||||||
|
* @throws PatchException If the fingerprint has not been matched.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val originalClassDef
|
||||||
|
get() = match().originalClassDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The matching method.
|
||||||
|
*
|
||||||
|
* @throws PatchException If the fingerprint has not been matched.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val originalMethod
|
||||||
|
get() = match().originalMethod
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mutable version of [originalClassDef].
|
||||||
|
*
|
||||||
|
* Accessing this property allocates a new mutable instance.
|
||||||
|
* Use [originalClassDef] if mutable access is not required.
|
||||||
|
*
|
||||||
|
* @throws PatchException If the fingerprint has not been matched.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val classDef
|
||||||
|
get() = match().classDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mutable version of [originalMethod].
|
||||||
|
*
|
||||||
|
* Accessing this property allocates a new mutable instance.
|
||||||
|
* Use [originalMethod] if mutable access is not required.
|
||||||
|
*
|
||||||
|
* @throws PatchException If the fingerprint has not been matched.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val method
|
||||||
|
get() = match().method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The match for the opcode pattern.
|
||||||
|
*
|
||||||
|
* @throws PatchException If the fingerprint has not been matched.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
@Deprecated("Instead use instructionMatch")
|
||||||
|
val patternMatch
|
||||||
|
get() = match().patternMatch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instruction filter matches.
|
||||||
|
*
|
||||||
|
* @throws PatchException If the fingerprint has not been matched.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
val instructionMatches
|
||||||
|
get() = match().instructionMatches
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The matches for the strings declared using `strings()`.
|
||||||
|
* This does not give matches for strings declared using [string] instruction filters.
|
||||||
|
*
|
||||||
|
* @throws PatchException If the fingerprint has not been matched.
|
||||||
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
|
@Deprecated("Instead use string instructions and `instructionMatches()`")
|
||||||
|
val stringMatches
|
||||||
|
get() = match().stringMatches
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A match of a [Fingerprint].
|
||||||
|
*
|
||||||
|
* @param originalClassDef The class the matching method is a member of.
|
||||||
|
* @param originalMethod The matching method.
|
||||||
|
* @param _instructionMatches The match for the instruction filters.
|
||||||
|
* @param _stringMatches The matches for the strings declared using `strings()`.
|
||||||
|
*/
|
||||||
|
class Match internal constructor(
|
||||||
|
val context: BytecodePatchContext,
|
||||||
|
val originalClassDef: ClassDef,
|
||||||
|
val originalMethod: Method,
|
||||||
|
private val _instructionMatches: List<InstructionMatch>?,
|
||||||
|
private val _stringMatches: List<StringMatch>?,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* The mutable version of [originalClassDef].
|
||||||
|
*
|
||||||
|
* Accessing this property allocates a new mutable instance.
|
||||||
|
* Use [originalClassDef] if mutable access is not required.
|
||||||
|
*/
|
||||||
|
val classDef by lazy {
|
||||||
|
with(context) {
|
||||||
|
originalClassDef.mutable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mutable version of [originalMethod].
|
||||||
|
*
|
||||||
|
* Accessing this property allocates a new mutable instance.
|
||||||
|
* Use [originalMethod] if mutable access is not required.
|
||||||
|
*/
|
||||||
|
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
|
||||||
|
|
||||||
|
@Deprecated("Instead use instructionMatches", ReplaceWith("instructionMatches"))
|
||||||
|
val patternMatch by lazy {
|
||||||
|
if (_instructionMatches == null) throw PatchException("Did not match $this")
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
PatternMatch(_instructionMatches.first().index, _instructionMatches.last().index)
|
||||||
|
}
|
||||||
|
|
||||||
|
val instructionMatches
|
||||||
|
get() = _instructionMatches ?: throw PatchException("Fingerprint declared no instruction filters")
|
||||||
|
val instructionMatchesOrNull = _instructionMatches
|
||||||
|
|
||||||
|
@Deprecated("Instead use string instructions and `instructionMatches()`")
|
||||||
|
val stringMatches
|
||||||
|
get() = _stringMatches ?: throw PatchException("Fingerprint declared no strings")
|
||||||
|
|
||||||
|
@Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
|
||||||
|
val stringMatchesOrNull = _stringMatches
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@Deprecated("Instead use InstructionMatch")
|
||||||
|
class PatternMatch internal constructor(
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
@Deprecated("Instead use string instructions and `InstructionMatch`")
|
||||||
|
class StringMatch internal constructor(val string: String, val index: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A match for a [InstructionFilter].
|
||||||
|
* @param filter The filter that matched
|
||||||
|
* @param index The instruction index it matched with.
|
||||||
|
* @param instruction The instruction that matched.
|
||||||
|
*/
|
||||||
|
class InstructionMatch internal constructor(
|
||||||
|
val filter: InstructionFilter,
|
||||||
|
val index: Int,
|
||||||
|
val instruction: Instruction
|
||||||
|
) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T> getInstruction(): T = instruction as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 instructionFilters Filters to match the method instructions.
|
||||||
|
* @property strings A list of the strings compared each using [String.contains].
|
||||||
|
* @property customBlock A custom condition for this fingerprint.
|
||||||
|
*
|
||||||
|
* @constructor Create a new [FingerprintBuilder].
|
||||||
|
*/
|
||||||
|
class FingerprintBuilder() {
|
||||||
|
private var accessFlags: Int? = null
|
||||||
|
private var returnType: String? = null
|
||||||
|
private var parameters: List<String>? = null
|
||||||
|
private var instructionFilters: List<InstructionFilter>? = 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.
|
||||||
|
*
|
||||||
|
* If [accessFlags] includes [AccessFlags.CONSTRUCTOR], then there is no need to
|
||||||
|
* set a return type set since constructors are always void return type.
|
||||||
|
*
|
||||||
|
* @param returnType The return type compared using [String.startsWith].
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyNoFiltersSet() {
|
||||||
|
if (this.instructionFilters != null) {
|
||||||
|
throw PatchException("Instruction filters already set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pattern of opcodes, where each opcode must appear immediately after the previous.
|
||||||
|
*
|
||||||
|
* To use opcodes with other [InstructionFilter] objects,
|
||||||
|
* instead use [instructions] with individual opcodes declared using [opcode].
|
||||||
|
*
|
||||||
|
* This method is identical to declaring individual opcode filters
|
||||||
|
* with [InstructionFilter.location] set to [InstructionLocation.MatchAfterImmediately]
|
||||||
|
* for all but the first opcode.
|
||||||
|
*
|
||||||
|
* Unless absolutely necessary, it is recommended to instead use [instructions]
|
||||||
|
* with more fine grained filters.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* opcodes(
|
||||||
|
* Opcode.INVOKE_VIRTUAL, // First opcode matches anywhere in the method.
|
||||||
|
* Opcode.MOVE_RESULT_OBJECT, // Must match exactly after INVOKE_VIRTUAL.
|
||||||
|
* Opcode.IPUT_OBJECT // Must match exactly after MOVE_RESULT_OBJECT.
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
* is identical to:
|
||||||
|
* ```
|
||||||
|
* instructions(
|
||||||
|
* opcode(Opcode.INVOKE_VIRTUAL), // First opcode matches anywhere in the method.
|
||||||
|
* opcode(Opcode.MOVE_RESULT_OBJECT, maxAfter = 0), // Must match exactly after INVOKE_VIRTUAL.
|
||||||
|
* opcode(Opcode.IPUT_OBJECT, maxAfter = 0) // Must match exactly after MOVE_RESULT_OBJECT.
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param opcodes An opcode pattern of instructions.
|
||||||
|
* Wildcard or unknown opcodes can be specified by `null`.
|
||||||
|
*/
|
||||||
|
fun opcodes(vararg opcodes: Opcode?) {
|
||||||
|
verifyNoFiltersSet()
|
||||||
|
if (opcodes.isEmpty()) throw IllegalArgumentException("One or more opcodes is required")
|
||||||
|
|
||||||
|
this.instructionFilters = OpcodesFilter.listOfOpcodes(opcodes.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pattern of opcodes from SMALI formatted text,
|
||||||
|
* where each opcode must appear immediately after the previous opcode.
|
||||||
|
*
|
||||||
|
* Unless absolutely necessary, it is recommended to instead use [instructions].
|
||||||
|
*
|
||||||
|
* @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) {
|
||||||
|
verifyNoFiltersSet()
|
||||||
|
if (instructions.isBlank()) throw IllegalArgumentException("No instructions declared (empty string)")
|
||||||
|
|
||||||
|
this.instructionFilters = OpcodesFilter.listOfOpcodes(
|
||||||
|
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 IllegalArgumentException("Unknown opcode: $name")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of instruction filters to match.
|
||||||
|
*/
|
||||||
|
fun instructions(vararg instructionFilters: InstructionFilter) {
|
||||||
|
verifyNoFiltersSet()
|
||||||
|
if (instructionFilters.isEmpty()) throw IllegalArgumentException("One or more instructions is required")
|
||||||
|
|
||||||
|
this.instructionFilters = instructionFilters.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the strings.
|
||||||
|
*
|
||||||
|
* @param strings A list of strings compared each using [String.contains].
|
||||||
|
*/
|
||||||
|
@Deprecated("Instead use `instruction()` filters and `string()` instruction declarations")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun build(): Fingerprint {
|
||||||
|
// If access flags include constructor then
|
||||||
|
// skip the return type check since it's always void.
|
||||||
|
if (returnType?.equals("V") == true && accessFlags != null
|
||||||
|
&& AccessFlags.CONSTRUCTOR.isSet(accessFlags!!)
|
||||||
|
) {
|
||||||
|
returnType = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fingerprint(
|
||||||
|
accessFlags,
|
||||||
|
returnType,
|
||||||
|
parameters,
|
||||||
|
instructionFilters,
|
||||||
|
strings,
|
||||||
|
customBlock,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val opcodesByName = Opcode.entries.associateBy { it.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fingerprint(
|
||||||
|
block: FingerprintBuilder.() -> Unit,
|
||||||
|
) = FingerprintBuilder().apply(block).build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches two lists of parameters, where the first parameter list
|
||||||
|
* starts with the values of the second list.
|
||||||
|
*/
|
||||||
|
internal fun parametersStartsWith(
|
||||||
|
targetMethodParameters: Iterable<CharSequence>,
|
||||||
|
fingerprintParameters: Iterable<CharSequence>,
|
||||||
|
): Boolean {
|
||||||
|
if (fingerprintParameters.count() != targetMethodParameters.count()) return false
|
||||||
|
val fingerprintIterator = fingerprintParameters.iterator()
|
||||||
|
|
||||||
|
targetMethodParameters.forEach {
|
||||||
|
if (!it.startsWith(fingerprintIterator.next())) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
983
src/main/kotlin/app/revanced/patcher/InstructionFilter.kt
Normal file
983
src/main/kotlin/app/revanced/patcher/InstructionFilter.kt
Normal file
@@ -0,0 +1,983 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.FieldAccessFilter.Companion.parseJvmFieldAccess
|
||||||
|
import app.revanced.patcher.MethodCallFilter.Companion.parseJvmMethodCall
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
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.instruction.WideLiteralInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple interface to control how much space is allowed between a previous
|
||||||
|
* [InstructionFilter match and the current [InstructionFilter].
|
||||||
|
*/
|
||||||
|
fun interface InstructionLocation {
|
||||||
|
/**
|
||||||
|
* @param previouslyMatchedIndex The previously matched index, or -1 if this is the first filter.
|
||||||
|
* @param currentIndex The current method index that is about to be checked.
|
||||||
|
*/
|
||||||
|
fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matching can occur anywhere after the previous instruction filter match index.
|
||||||
|
* Is the default behavior for all filters.
|
||||||
|
*/
|
||||||
|
class MatchAfterAnywhere : InstructionLocation {
|
||||||
|
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the first instruction of a method.
|
||||||
|
*
|
||||||
|
* This can only be used for the first filter, and using with any other filter will throw an exception.
|
||||||
|
*/
|
||||||
|
class MatchFirst() : InstructionLocation {
|
||||||
|
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean {
|
||||||
|
if (previouslyMatchedIndex >= 0) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"MatchFirst can only be used for the first instruction filter"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instruction index immediately after the previous filter.
|
||||||
|
*
|
||||||
|
* Useful for opcodes that must always appear immediately after the last filter such as:
|
||||||
|
* - [Opcode.MOVE_RESULT]
|
||||||
|
* - [Opcode.MOVE_RESULT_WIDE]
|
||||||
|
* - [Opcode.MOVE_RESULT_OBJECT]
|
||||||
|
*
|
||||||
|
* This cannot be used for the first filter and will throw an exception.
|
||||||
|
*/
|
||||||
|
class MatchAfterImmediately() : InstructionLocation {
|
||||||
|
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean {
|
||||||
|
if (previouslyMatchedIndex < 0) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"MatchAfterImmediately cannot be used for the first instruction filter"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return currentIndex - 1 == previouslyMatchedIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instruction index can occur within a range of the previous instruction filter match index.
|
||||||
|
* used to constrain instruction matching to a region after the previous instruction filter.
|
||||||
|
*
|
||||||
|
* This cannot be used for the first filter and will throw an exception.
|
||||||
|
*
|
||||||
|
* @param matchDistance The number of unmatched instructions that can exist between the
|
||||||
|
* current instruction filter and the previously matched instruction filter.
|
||||||
|
* A value of 0 means the current filter can only match immediately after
|
||||||
|
* the previously matched instruction (making this functionally identical to
|
||||||
|
* [MatchAfterImmediately]). A value of 10 means between 0 and 10 unmatched
|
||||||
|
* instructions can exist between the previously matched instruction and
|
||||||
|
* the current instruction filter.
|
||||||
|
*/
|
||||||
|
class MatchAfterWithin(var matchDistance: Int) : InstructionLocation {
|
||||||
|
init {
|
||||||
|
if (matchDistance < 0) {
|
||||||
|
throw IllegalArgumentException("matchDistance must be non-negative")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean {
|
||||||
|
if (previouslyMatchedIndex < 0) {
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"MatchAfterImmediately cannot be used for the first instruction filter"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return currentIndex - previouslyMatchedIndex - 1 <= matchDistance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches method [Instruction] objects, similar to how [Fingerprint] matches entire fingerprints.
|
||||||
|
*
|
||||||
|
* The most basic filters match only opcodes and nothing more,
|
||||||
|
* and more precise filters can match:
|
||||||
|
* - Field references (get/put opcodes) by name/type.
|
||||||
|
* - Method calls (invoke_* opcodes) by name/parameter/return type.
|
||||||
|
* - Object instantiation for specific class types.
|
||||||
|
* - Literal const values.
|
||||||
|
*/
|
||||||
|
fun interface InstructionFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [InstructionLocation] associated with this filter.
|
||||||
|
*/
|
||||||
|
val location: InstructionLocation
|
||||||
|
get() = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this filter matches the method instruction.
|
||||||
|
*
|
||||||
|
* @param enclosingMethod The method of that contains [instruction].
|
||||||
|
* @param instruction The instruction to check for a match.
|
||||||
|
*/
|
||||||
|
fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AnyInstruction internal constructor(
|
||||||
|
internal val filters: List<InstructionFilter>,
|
||||||
|
override val location : InstructionLocation
|
||||||
|
) : InstructionFilter {
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
) : Boolean {
|
||||||
|
return filters.any { filter ->
|
||||||
|
filter.matches(enclosingMethod, instruction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logical OR operator where the first filter that matches satisfies this filter.
|
||||||
|
*/
|
||||||
|
fun anyInstruction(
|
||||||
|
vararg filters: InstructionFilter,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = AnyInstruction(filters.asList(), location)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
open class OpcodeFilter(
|
||||||
|
val opcode: Opcode,
|
||||||
|
override val location : InstructionLocation
|
||||||
|
) : InstructionFilter {
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
return instruction.opcode == opcode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single opcode.
|
||||||
|
*/
|
||||||
|
fun opcode(
|
||||||
|
opcode: Opcode,
|
||||||
|
location: InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = OpcodeFilter(opcode, location)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a single instruction from many kinds of opcodes.
|
||||||
|
* If matching only a single opcode instead use [OpcodeFilter].
|
||||||
|
*/
|
||||||
|
open class OpcodesFilter private constructor(
|
||||||
|
val opcodes: EnumSet<Opcode>?,
|
||||||
|
override val location : InstructionLocation
|
||||||
|
) : InstructionFilter {
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
/**
|
||||||
|
* Value of `null` will match any opcode.
|
||||||
|
*/
|
||||||
|
opcodes: List<Opcode>?,
|
||||||
|
location : InstructionLocation
|
||||||
|
) : this(if (opcodes == null) null else EnumSet.copyOf(opcodes), location)
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
if (opcodes == null) {
|
||||||
|
return true // Match anything.
|
||||||
|
}
|
||||||
|
return opcodes.contains(instruction.opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* First opcode can match anywhere in a method, but all
|
||||||
|
* subsequent opcodes must match after the previous opcode.
|
||||||
|
*
|
||||||
|
* A value of `null` indicates to match any opcode.
|
||||||
|
*/
|
||||||
|
internal fun listOfOpcodes(opcodes: Collection<Opcode?>): List<InstructionFilter> {
|
||||||
|
val list = ArrayList<InstructionFilter>(opcodes.size)
|
||||||
|
var location: InstructionLocation? = null
|
||||||
|
|
||||||
|
opcodes.forEach { opcode ->
|
||||||
|
// First opcode can match anywhere.
|
||||||
|
val opcodeLocation = location ?: InstructionLocation.MatchAfterAnywhere()
|
||||||
|
|
||||||
|
list += if (opcode == null) {
|
||||||
|
// Null opcode matches anything.
|
||||||
|
OpcodesFilter(
|
||||||
|
null as List<Opcode>?,
|
||||||
|
opcodeLocation
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
OpcodeFilter(opcode, opcodeLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location == null) {
|
||||||
|
location = InstructionLocation.MatchAfterImmediately()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class LiteralFilter internal constructor(
|
||||||
|
var literal: () -> Long,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation
|
||||||
|
) : OpcodesFilter(opcodes, location) {
|
||||||
|
|
||||||
|
private var literalValue: Long? = null
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
if (!super.matches(enclosingMethod, instruction)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instruction !is WideLiteralInstruction) return false
|
||||||
|
|
||||||
|
if (literalValue == null) {
|
||||||
|
literalValue = literal()
|
||||||
|
}
|
||||||
|
|
||||||
|
return instruction.wideLiteral == literalValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Literal value, such as:
|
||||||
|
* `const v1, 0x7f080318`
|
||||||
|
*
|
||||||
|
* that can be matched using:
|
||||||
|
* `LiteralFilter(0x7f080318)`
|
||||||
|
* or
|
||||||
|
* `LiteralFilter(2131231512)`
|
||||||
|
*/
|
||||||
|
fun literal(
|
||||||
|
literal: () -> Long,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = LiteralFilter(literal, opcodes, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Literal value, such as:
|
||||||
|
* `const v1, 0x7f080318`
|
||||||
|
*
|
||||||
|
* that can be matched using:
|
||||||
|
* `LiteralFilter(0x7f080318)`
|
||||||
|
* or
|
||||||
|
* `LiteralFilter(2131231512L)`
|
||||||
|
*/
|
||||||
|
fun literal(
|
||||||
|
literal: Long,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = LiteralFilter({ literal }, opcodes, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integer point literal.
|
||||||
|
*/
|
||||||
|
fun literal(
|
||||||
|
literal: Int,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = LiteralFilter({ literal.toLong() }, opcodes, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Double point literal.
|
||||||
|
*/
|
||||||
|
fun literal(
|
||||||
|
literal: Double,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = LiteralFilter({ literal.toRawBits() }, opcodes, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Floating point literal.
|
||||||
|
*/
|
||||||
|
fun literal(
|
||||||
|
literal: Float,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = LiteralFilter({ literal.toRawBits().toLong() }, opcodes, location)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
enum class StringMatchType {
|
||||||
|
EQUALS,
|
||||||
|
CONTAINS,
|
||||||
|
STARTS_WITH,
|
||||||
|
ENDS_WITH
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringFilter internal constructor(
|
||||||
|
var string: () -> String,
|
||||||
|
var matchType: StringMatchType,
|
||||||
|
location : InstructionLocation
|
||||||
|
) : OpcodesFilter(listOf(Opcode.CONST_STRING, Opcode.CONST_STRING_JUMBO), location) {
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
if (!super.matches(enclosingMethod, instruction)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val instructionString = ((instruction as ReferenceInstruction).reference as StringReference).string
|
||||||
|
val filterString = string()
|
||||||
|
|
||||||
|
return when (matchType) {
|
||||||
|
StringMatchType.EQUALS -> instructionString == filterString
|
||||||
|
StringMatchType.CONTAINS -> instructionString.contains(filterString)
|
||||||
|
StringMatchType.STARTS_WITH -> instructionString.startsWith(filterString)
|
||||||
|
StringMatchType.ENDS_WITH -> instructionString.endsWith(filterString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Literal String instruction.
|
||||||
|
*/
|
||||||
|
fun string(
|
||||||
|
string: () -> String,
|
||||||
|
/**
|
||||||
|
* If [string] is a partial match, where the target string contains this string.
|
||||||
|
* For more precise matching, consider using [anyInstruction] with multiple exact string declarations.
|
||||||
|
*/
|
||||||
|
matchType: StringMatchType = StringMatchType.EQUALS,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = StringFilter(string, matchType, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Literal String instruction.
|
||||||
|
*/
|
||||||
|
fun string(
|
||||||
|
string: String,
|
||||||
|
/**
|
||||||
|
* How to compare [string] against the string constant opcode. For more precise matching
|
||||||
|
* of multiple strings, consider using [anyInstruction] with multiple exact string declarations.
|
||||||
|
*/
|
||||||
|
matchType: StringMatchType = StringMatchType.EQUALS,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = StringFilter({ string }, matchType, location)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MethodCallFilter internal constructor(
|
||||||
|
val definingClass: (() -> String)? = null,
|
||||||
|
val name: (() -> String)? = null,
|
||||||
|
val parameters: (() -> List<String>)? = null,
|
||||||
|
val returnType: (() -> String)? = null,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation
|
||||||
|
) : OpcodesFilter(opcodes, location) {
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
if (!super.matches(enclosingMethod, instruction)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val reference = (instruction as? ReferenceInstruction)?.reference as? MethodReference
|
||||||
|
if (reference == null) return false
|
||||||
|
|
||||||
|
if (definingClass != null) {
|
||||||
|
val referenceClass = reference.definingClass
|
||||||
|
val definingClass = definingClass()
|
||||||
|
|
||||||
|
if (!referenceClass.endsWith(definingClass)) {
|
||||||
|
// Check if 'this' defining class is used.
|
||||||
|
// Would be nice if this also checked all super classes,
|
||||||
|
// but doing so requires iteratively checking all superclasses
|
||||||
|
// up to the root class since class defs are mere Strings.
|
||||||
|
if (!(definingClass == "this" && referenceClass == enclosingMethod.definingClass)) {
|
||||||
|
return false
|
||||||
|
} // else, the method call is for 'this' class.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name != null && reference.name != name()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (returnType != null && !reference.returnType.startsWith(returnType())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (parameters != null && !parametersStartsWith(reference.parameterTypes, parameters())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val regex = Regex("""^(L[^;]+;)->([^(\s]+)\(([^)]*)\)(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""")
|
||||||
|
|
||||||
|
internal fun parseJvmMethodCall(
|
||||||
|
methodSignature: String,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
): MethodCallFilter {
|
||||||
|
val matchResult = regex.matchEntire(methodSignature)
|
||||||
|
?: throw IllegalArgumentException("Invalid method signature: $methodSignature")
|
||||||
|
|
||||||
|
val classDescriptor = matchResult.groupValues[1]
|
||||||
|
val methodName = matchResult.groupValues[2]
|
||||||
|
val paramDescriptorString = matchResult.groupValues[3]
|
||||||
|
val returnDescriptor = matchResult.groupValues[4]
|
||||||
|
|
||||||
|
val paramDescriptors = parseParameterDescriptors(paramDescriptorString)
|
||||||
|
|
||||||
|
return MethodCallFilter(
|
||||||
|
{ classDescriptor },
|
||||||
|
{ methodName },
|
||||||
|
{ paramDescriptors },
|
||||||
|
{ returnDescriptor },
|
||||||
|
opcodes,
|
||||||
|
location
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a single JVM type descriptor or an array descriptor at the current position.
|
||||||
|
* For example: Lcom/example/SomeClass; or I or [I or [Lcom/example/SomeClass;
|
||||||
|
*/
|
||||||
|
private fun parseSingleType(params: String, startIndex: Int): Pair<String, Int> {
|
||||||
|
var i = startIndex
|
||||||
|
|
||||||
|
// Skip past array declaration, including multi-dimensional arrays.
|
||||||
|
val paramsLength = params.length
|
||||||
|
while (i < paramsLength && params[i] == '[') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (i < paramsLength && params[i] == 'L') {
|
||||||
|
// It's an object type starting with 'L', read until ';'
|
||||||
|
val semicolonPos = params.indexOf(';', i)
|
||||||
|
if (semicolonPos < 0) {
|
||||||
|
throw IllegalArgumentException("Malformed object descriptor (missing semicolon): $params")
|
||||||
|
}
|
||||||
|
// Substring from startIndex up to and including the semicolon.
|
||||||
|
val typeDescriptor = params.substring(startIndex, semicolonPos + 1)
|
||||||
|
typeDescriptor to (semicolonPos + 1)
|
||||||
|
} else {
|
||||||
|
// It's either a primitive or we've already consumed the array part
|
||||||
|
// So just take one character (e.g. 'I', 'Z', 'B', etc.)
|
||||||
|
val typeDescriptor = params.substring(startIndex, i + 1)
|
||||||
|
typeDescriptor to (i + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the parameters into a list of JVM type descriptors.
|
||||||
|
*/
|
||||||
|
private fun parseParameterDescriptors(paramString: String): List<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
var currentIndex = 0
|
||||||
|
val stringLength = paramString.length
|
||||||
|
|
||||||
|
while (currentIndex < stringLength) {
|
||||||
|
val (type, nextIndex) = parseSingleType(paramString, currentIndex)
|
||||||
|
result.add(type)
|
||||||
|
currentIndex = nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifies method calls.
|
||||||
|
*
|
||||||
|
* `Null` parameters matches anything.
|
||||||
|
*
|
||||||
|
* By default any type of method call matches.
|
||||||
|
* Specify opcodes if a specific type of method call is desired (such as only static calls).
|
||||||
|
*/
|
||||||
|
fun methodCall(
|
||||||
|
/**
|
||||||
|
* Defining class of the method call. Matches using endsWith().
|
||||||
|
*
|
||||||
|
* For calls to a method in the same class, use 'this' as the defining class.
|
||||||
|
* Note: 'this' does not work for methods declared only in a superclass.
|
||||||
|
*/
|
||||||
|
definingClass: (() -> String)? = null,
|
||||||
|
/**
|
||||||
|
* Method name. Must be exact match of the method name.
|
||||||
|
*/
|
||||||
|
name: (() -> String)? = null,
|
||||||
|
/**
|
||||||
|
* Parameters of the method call. Each parameter matches
|
||||||
|
* using startsWith() and semantics are the same as [Fingerprint].
|
||||||
|
*/
|
||||||
|
parameters: (() -> List<String>)? = null,
|
||||||
|
/**
|
||||||
|
* Return type. Matches using startsWith()
|
||||||
|
*/
|
||||||
|
returnType: (() -> String)? = null,
|
||||||
|
/**
|
||||||
|
* Opcode types to match. By default this matches any method call opcode:
|
||||||
|
* `Opcode.INVOKE_*`.
|
||||||
|
*
|
||||||
|
* If this filter must match specific types of method call, then specify the desired opcodes
|
||||||
|
* such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
|
||||||
|
*/
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
/**
|
||||||
|
* The locations where this filter is allowed to match.
|
||||||
|
*/
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = MethodCallFilter(
|
||||||
|
definingClass,
|
||||||
|
name,
|
||||||
|
parameters,
|
||||||
|
returnType,
|
||||||
|
opcodes,
|
||||||
|
location
|
||||||
|
)
|
||||||
|
|
||||||
|
fun methodCall(
|
||||||
|
/**
|
||||||
|
* Defining class of the method call. Matches using endsWith().
|
||||||
|
*
|
||||||
|
* For calls to a method in the same class, use 'this' as the defining class.
|
||||||
|
* Note: 'this' does not work for methods declared only in a superclass.
|
||||||
|
*/
|
||||||
|
definingClass: String? = null,
|
||||||
|
/**
|
||||||
|
* Method name. Must be exact match of the method name.
|
||||||
|
*/
|
||||||
|
name: String? = null,
|
||||||
|
/**
|
||||||
|
* Parameters of the method call. Each parameter matches
|
||||||
|
* using startsWith() and semantics are the same as [Fingerprint].
|
||||||
|
*/
|
||||||
|
parameters: List<String>? = null,
|
||||||
|
/**
|
||||||
|
* Return type. Matches using startsWith()
|
||||||
|
*/
|
||||||
|
returnType: String? = null,
|
||||||
|
/**
|
||||||
|
* Opcode types to match. By default this matches any method call opcode:
|
||||||
|
* `Opcode.INVOKE_*`.
|
||||||
|
*
|
||||||
|
* If this filter must match specific types of method call, then specify the desired opcodes
|
||||||
|
* such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
|
||||||
|
*/
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
/**
|
||||||
|
* The locations where this filter is allowed to match.
|
||||||
|
*/
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = MethodCallFilter(
|
||||||
|
if (definingClass != null) {
|
||||||
|
{ definingClass }
|
||||||
|
} else null,
|
||||||
|
if (name != null) {
|
||||||
|
{ name }
|
||||||
|
} else null,
|
||||||
|
if (parameters != null) {
|
||||||
|
{ parameters }
|
||||||
|
} else null,
|
||||||
|
if (returnType != null) {
|
||||||
|
{ returnType }
|
||||||
|
} else null,
|
||||||
|
opcodes,
|
||||||
|
location
|
||||||
|
)
|
||||||
|
|
||||||
|
fun methodCall(
|
||||||
|
/**
|
||||||
|
* Defining class of the method call. Matches using endsWith().
|
||||||
|
*
|
||||||
|
* For calls to a method in the same class, use 'this' as the defining class.
|
||||||
|
* Note: 'this' does not work for methods declared only in a superclass.
|
||||||
|
*/
|
||||||
|
definingClass: String? = null,
|
||||||
|
/**
|
||||||
|
* Method name. Must be exact match of the method name.
|
||||||
|
*/
|
||||||
|
name: String? = null,
|
||||||
|
/**
|
||||||
|
* Parameters of the method call. Each parameter matches
|
||||||
|
* using startsWith() and semantics are the same as [Fingerprint].
|
||||||
|
*/
|
||||||
|
parameters: List<String>? = null,
|
||||||
|
/**
|
||||||
|
* Return type. Matches using startsWith()
|
||||||
|
*/
|
||||||
|
returnType: String? = null,
|
||||||
|
/**
|
||||||
|
* Opcode types to match. By default this matches any method call opcode:
|
||||||
|
* `Opcode.INVOKE_*`.
|
||||||
|
*
|
||||||
|
* If this filter must match specific types of method call, then specify the desired opcodes
|
||||||
|
* such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
|
||||||
|
*/
|
||||||
|
opcode: Opcode,
|
||||||
|
/**
|
||||||
|
* The locations where this filter is allowed to match.
|
||||||
|
*/
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = MethodCallFilter(
|
||||||
|
if (definingClass != null) {
|
||||||
|
{ definingClass }
|
||||||
|
} else null,
|
||||||
|
if (name != null) {
|
||||||
|
{ name }
|
||||||
|
} else null,
|
||||||
|
if (parameters != null) {
|
||||||
|
{ parameters }
|
||||||
|
} else null,
|
||||||
|
if (returnType != null) {
|
||||||
|
{ returnType }
|
||||||
|
} else null,
|
||||||
|
listOf(opcode),
|
||||||
|
location
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method call for a copy pasted SMALI style method signature. e.g.:
|
||||||
|
* `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`
|
||||||
|
*
|
||||||
|
* Does not support obfuscated method names or parameter/return types.
|
||||||
|
*/
|
||||||
|
fun methodCall(
|
||||||
|
smali: String,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = parseJvmMethodCall(smali, opcodes, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method call for a copy pasted SMALI style method signature. e.g.:
|
||||||
|
* `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`
|
||||||
|
*
|
||||||
|
* Does not support obfuscated method names or parameter/return types.
|
||||||
|
*/
|
||||||
|
fun methodCall(
|
||||||
|
smali: String,
|
||||||
|
opcode: Opcode,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = parseJvmMethodCall(smali, listOf(opcode), location)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class FieldAccessFilter internal constructor(
|
||||||
|
val definingClass: (() -> String)? = null,
|
||||||
|
val name: (() -> String)? = null,
|
||||||
|
val type: (() -> String)? = null,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation
|
||||||
|
) : OpcodesFilter(opcodes, location) {
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
if (!super.matches(enclosingMethod, instruction)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val reference = (instruction as? ReferenceInstruction)?.reference as? FieldReference
|
||||||
|
if (reference == null) return false
|
||||||
|
|
||||||
|
if (definingClass != null) {
|
||||||
|
val referenceClass = reference.definingClass
|
||||||
|
val definingClass = definingClass()
|
||||||
|
|
||||||
|
if (!referenceClass.endsWith(definingClass)) {
|
||||||
|
if (!(definingClass == "this" && referenceClass == enclosingMethod.definingClass)) {
|
||||||
|
return false
|
||||||
|
} // else, the method call is for 'this' class.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name != null && reference.name != name()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (type != null && !reference.type.startsWith(type())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
internal companion object {
|
||||||
|
private val regex = Regex("""^(L[^;]+;)->([^:]+):(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""")
|
||||||
|
|
||||||
|
internal fun parseJvmFieldAccess(
|
||||||
|
fieldSignature: String,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
): FieldAccessFilter {
|
||||||
|
val matchResult = regex.matchEntire(fieldSignature)
|
||||||
|
?: throw IllegalArgumentException("Invalid field access smali: $fieldSignature")
|
||||||
|
|
||||||
|
return fieldAccess(
|
||||||
|
definingClass = matchResult.groupValues[1],
|
||||||
|
name = matchResult.groupValues[2],
|
||||||
|
type = matchResult.groupValues[3],
|
||||||
|
opcodes = opcodes,
|
||||||
|
location = location
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a field call, such as:
|
||||||
|
* `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
|
||||||
|
*/
|
||||||
|
fun fieldAccess(
|
||||||
|
/**
|
||||||
|
* Defining class of the field call. Matches using endsWith().
|
||||||
|
*
|
||||||
|
* For calls to a method in the same class, use 'this' as the defining class.
|
||||||
|
* Note: 'this' does not work for fields found in superclasses.
|
||||||
|
*/
|
||||||
|
definingClass: (() -> String)? = null,
|
||||||
|
/**
|
||||||
|
* Name of the field. Must be a full match of the field name.
|
||||||
|
*/
|
||||||
|
name: (() -> String)? = null,
|
||||||
|
/**
|
||||||
|
* Class type of field. Partial matches using startsWith() is allowed.
|
||||||
|
*/
|
||||||
|
type: (() -> String)? = null,
|
||||||
|
/**
|
||||||
|
* Valid opcodes matches for this instruction.
|
||||||
|
* By default this matches any kind of field access
|
||||||
|
* (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc).
|
||||||
|
*/
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
/**
|
||||||
|
* The locations where this filter is allowed to match.
|
||||||
|
*/
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = FieldAccessFilter(definingClass, name, type, opcodes, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a field call, such as:
|
||||||
|
* `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
|
||||||
|
*/
|
||||||
|
fun fieldAccess(
|
||||||
|
/**
|
||||||
|
* Defining class of the field call. Matches using endsWith().
|
||||||
|
*
|
||||||
|
* For calls to a method in the same class, use 'this' as the defining class.
|
||||||
|
* Note: 'this' does not work for fields found in superclasses.
|
||||||
|
*/
|
||||||
|
definingClass: String? = null,
|
||||||
|
/**
|
||||||
|
* Name of the field. Must be a full match of the field name.
|
||||||
|
*/
|
||||||
|
name: String? = null,
|
||||||
|
/**
|
||||||
|
* Class type of field. Partial matches using startsWith() is allowed.
|
||||||
|
*/
|
||||||
|
type: String? = null,
|
||||||
|
/**
|
||||||
|
* Valid opcodes matches for this instruction.
|
||||||
|
* By default this matches any kind of field access
|
||||||
|
* (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc).
|
||||||
|
*/
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
/**
|
||||||
|
* The locations where this filter is allowed to match.
|
||||||
|
*/
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = FieldAccessFilter(
|
||||||
|
if (definingClass != null) {
|
||||||
|
{ definingClass }
|
||||||
|
} else null,
|
||||||
|
if (name != null) {
|
||||||
|
{ name }
|
||||||
|
} else null,
|
||||||
|
if (type != null) {
|
||||||
|
{ type }
|
||||||
|
} else null,
|
||||||
|
opcodes,
|
||||||
|
location
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a field call, such as:
|
||||||
|
* `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
|
||||||
|
*/
|
||||||
|
fun fieldAccess(
|
||||||
|
/**
|
||||||
|
* Defining class of the field call. Matches using endsWith().
|
||||||
|
*
|
||||||
|
* For calls to a method in the same class, use 'this' as the defining class.
|
||||||
|
* Note: 'this' does not work for fields found in superclasses.
|
||||||
|
*/
|
||||||
|
definingClass: String? = null,
|
||||||
|
/**
|
||||||
|
* Name of the field. Must be a full match of the field name.
|
||||||
|
*/
|
||||||
|
name: String? = null,
|
||||||
|
/**
|
||||||
|
* Class type of field. Partial matches using startsWith() is allowed.
|
||||||
|
*/
|
||||||
|
type: String? = null,
|
||||||
|
opcode: Opcode,
|
||||||
|
/**
|
||||||
|
* The locations where this filter is allowed to match.
|
||||||
|
*/
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = fieldAccess(
|
||||||
|
definingClass,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
listOf(opcode),
|
||||||
|
location
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field access for a copy pasted SMALI style field access call. e.g.:
|
||||||
|
* `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`
|
||||||
|
*
|
||||||
|
* Does not support obfuscated field names or obfuscated field types.
|
||||||
|
*/
|
||||||
|
fun fieldAccess(
|
||||||
|
smali: String,
|
||||||
|
opcodes: List<Opcode>? = null,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = parseJvmFieldAccess(smali, opcodes, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field access for a copy pasted SMALI style field access call. e.g.:
|
||||||
|
* `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`
|
||||||
|
*
|
||||||
|
* Does not support obfuscated field names or obfuscated field types.
|
||||||
|
*/
|
||||||
|
fun fieldAccess(
|
||||||
|
smali: String,
|
||||||
|
opcode: Opcode,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = parseJvmFieldAccess(smali, listOf(opcode), location)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class NewInstanceFilter internal constructor (
|
||||||
|
var type: () -> String,
|
||||||
|
location : InstructionLocation
|
||||||
|
) : OpcodesFilter(listOf(Opcode.NEW_INSTANCE, Opcode.NEW_ARRAY), location) {
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
if (!super.matches(enclosingMethod, instruction)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val reference = (instruction as? ReferenceInstruction)?.reference as? TypeReference
|
||||||
|
if (reference == null) return false
|
||||||
|
|
||||||
|
return reference.type.endsWith(type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type.
|
||||||
|
*
|
||||||
|
* @param type Class type that matches the target instruction using [String.endsWith].
|
||||||
|
*/
|
||||||
|
fun newInstancetype(
|
||||||
|
type: () -> String,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = NewInstanceFilter(type, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type.
|
||||||
|
*
|
||||||
|
* @param type Class type that matches the target instruction using [String.endsWith].
|
||||||
|
*/
|
||||||
|
fun newInstance(
|
||||||
|
type: String,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) : NewInstanceFilter {
|
||||||
|
if (!type.endsWith(";")) {
|
||||||
|
throw IllegalArgumentException("Class type does not end with a semicolon: $type")
|
||||||
|
}
|
||||||
|
return NewInstanceFilter({ type }, location)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckCastFilter internal constructor (
|
||||||
|
var type: () -> String,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) : OpcodeFilter(Opcode.CHECK_CAST, location) {
|
||||||
|
|
||||||
|
override fun matches(
|
||||||
|
enclosingMethod: Method,
|
||||||
|
instruction: Instruction
|
||||||
|
): Boolean {
|
||||||
|
if (!super.matches(enclosingMethod, instruction)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val reference = (instruction as? ReferenceInstruction)?.reference as? TypeReference
|
||||||
|
if (reference == null) return false
|
||||||
|
|
||||||
|
return reference.type.endsWith(type())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type.
|
||||||
|
*
|
||||||
|
* @param type Class type that matches the target instruction using [String.endsWith].
|
||||||
|
*/
|
||||||
|
fun checkCast(
|
||||||
|
type: () -> String,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) = CheckCastFilter(type, location)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type.
|
||||||
|
*
|
||||||
|
* @param type Class type that matches the target instruction using [String.endsWith].
|
||||||
|
*/
|
||||||
|
fun checkCast(
|
||||||
|
type: String,
|
||||||
|
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
|
||||||
|
) : CheckCastFilter {
|
||||||
|
if (!type.endsWith(";")) {
|
||||||
|
throw IllegalArgumentException("Class type does not end with a semicolon: $type")
|
||||||
|
}
|
||||||
|
|
||||||
|
return CheckCastFilter({ type }, location)
|
||||||
|
}
|
||||||
@@ -4,15 +4,14 @@ package app.revanced.patcher
|
|||||||
|
|
||||||
import app.revanced.patcher.Matcher.MatchContext
|
import app.revanced.patcher.Matcher.MatchContext
|
||||||
import app.revanced.patcher.dex.mutable.MutableMethod
|
import app.revanced.patcher.dex.mutable.MutableMethod
|
||||||
import app.revanced.patcher.extensions.instructions
|
import app.revanced.patcher.extensions.*
|
||||||
import app.revanced.patcher.patch.BytecodePatchContext
|
import app.revanced.patcher.patch.BytecodePatchContext
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.HiddenApiRestriction
|
import com.android.tools.smali.dexlib2.HiddenApiRestriction
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.*
|
import com.android.tools.smali.dexlib2.iface.*
|
||||||
import com.android.tools.smali.dexlib2.iface.Annotation
|
import com.android.tools.smali.dexlib2.iface.Annotation
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
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
|
|
||||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||||
import kotlin.properties.ReadOnlyProperty
|
import kotlin.properties.ReadOnlyProperty
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
@@ -52,98 +51,93 @@ fun MethodImplementation.anyTryBlock(predicate: TryBlock<out ExceptionHandler>.(
|
|||||||
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate)
|
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate)
|
||||||
|
|
||||||
fun Iterable<Instruction>.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate)
|
fun Iterable<Instruction>.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate)
|
||||||
|
fun BytecodePatchContext.firstClassDefOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
|
with(MatchContext()) { classDefs.firstOrNull { it.predicate() } }
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDefOrNull(predicate: MatchPredicate<ClassDef>) =
|
fun BytecodePatchContext.firstClassDef(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
with(predicate) { with(MatchContext()) { classDefs.firstOrNull { it.match() } } }
|
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDef(predicate: MatchPredicate<ClassDef>) =
|
|
||||||
requireNotNull(firstClassDefOrNull(predicate))
|
requireNotNull(firstClassDefOrNull(predicate))
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: MatchPredicate<ClassDef>) =
|
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
firstClassDefOrNull(predicate)?.mutable()
|
firstClassDefOrNull(predicate)?.mutable()
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDefMutable(predicate: MatchPredicate<ClassDef>) =
|
fun BytecodePatchContext.firstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
requireNotNull(firstClassDefMutableOrNull(predicate))
|
requireNotNull(firstClassDefMutableOrNull(predicate))
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDefOrNull(
|
fun BytecodePatchContext.firstClassDefOrNull(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = lookupMaps.classDefsByType[type]?.takeIf {
|
) = lookupMaps.classDefsByType[type]?.takeIf {
|
||||||
predicate == null || with(predicate) { with(MatchContext()) { it.match() } }
|
predicate == null || with(MatchContext()) { it.predicate() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDef(
|
fun BytecodePatchContext.firstClassDef(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = requireNotNull(firstClassDefOrNull(type, predicate))
|
) = requireNotNull(firstClassDefOrNull(type, predicate))
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDefMutableOrNull(
|
fun BytecodePatchContext.firstClassDefMutableOrNull(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = firstClassDefOrNull(type, predicate)?.mutable()
|
) = firstClassDefOrNull(type, predicate)?.mutable()
|
||||||
|
|
||||||
fun BytecodePatchContext.firstClassDefMutable(
|
fun BytecodePatchContext.firstClassDefMutable(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = requireNotNull(firstClassDefMutableOrNull(type, predicate))
|
) = requireNotNull(firstClassDefMutableOrNull(type, predicate))
|
||||||
|
|
||||||
fun Iterable<ClassDef>.firstMethodOrNull(predicate: MatchPredicate<Method>) = with(predicate) {
|
fun Iterable<ClassDef>.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
with(MatchContext()) {
|
with(MatchContext()) {
|
||||||
this@firstMethodOrNull.asSequence().flatMap { it.methods.asSequence() }.firstOrNull { it.match() }
|
this@firstMethodOrNull.asSequence().flatMap { it.methods.asSequence() }.firstOrNull { it.predicate() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Iterable<ClassDef>.firstMethod(predicate: MatchPredicate<Method>) = requireNotNull(firstMethodOrNull(predicate))
|
fun Iterable<ClassDef>.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
|
requireNotNull(firstMethodOrNull(predicate))
|
||||||
|
|
||||||
context(BytecodePatchContext)
|
context(context: BytecodePatchContext)
|
||||||
fun Iterable<ClassDef>.firstMethodMutableOrNull(predicate: MatchPredicate<Method>): MutableMethod? {
|
fun Iterable<ClassDef>.firstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean): MutableMethod? =
|
||||||
with(predicate) {
|
with(context) {
|
||||||
with(MatchContext()) {
|
with(MatchContext()) {
|
||||||
this@firstMethodMutableOrNull.forEach { classDef ->
|
this@firstMethodMutableOrNull.forEach { classDef ->
|
||||||
classDef.methods.firstOrNull { it.match() }?.let { method ->
|
classDef.methods.firstOrNull { it.predicate() }?.let { method ->
|
||||||
return classDef.mutable().methods.first { MethodUtil.methodSignaturesMatch(it, method) }
|
return classDef.mutable().methods.first { MethodUtil.methodSignaturesMatch(it, method) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
context(_: BytecodePatchContext)
|
||||||
}
|
fun Iterable<ClassDef>.firstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
|
|
||||||
context(BytecodePatchContext)
|
|
||||||
fun Iterable<ClassDef>.firstMethodMutable(predicate: MatchPredicate<Method>) =
|
|
||||||
requireNotNull(firstMethodMutableOrNull(predicate))
|
requireNotNull(firstMethodMutableOrNull(predicate))
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethodOrNull(predicate: MatchPredicate<Method>) =
|
fun BytecodePatchContext.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
classDefs.firstMethodOrNull(predicate)
|
classDefs.firstMethodOrNull(predicate)
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethod(predicate: MatchPredicate<Method>) =
|
fun BytecodePatchContext.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
requireNotNull(firstMethodOrNull(predicate))
|
requireNotNull(firstMethodOrNull(predicate))
|
||||||
|
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: MatchPredicate<Method>) =
|
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
classDefs.firstMethodMutableOrNull(predicate)
|
classDefs.firstMethodMutableOrNull(predicate)
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethodMutable(predicate: MatchPredicate<Method>) =
|
fun BytecodePatchContext.firstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
requireNotNull(firstMethodMutableOrNull(predicate))
|
requireNotNull(firstMethodMutableOrNull(predicate))
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethodOrNull(
|
fun BytecodePatchContext.firstMethodOrNull(
|
||||||
vararg strings: String,
|
vararg strings: String,
|
||||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||||
) = with(predicate) {
|
) = with(MatchContext()) {
|
||||||
with(MatchContext()) {
|
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.firstOrNull { it.predicate() }
|
||||||
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.firstOrNull { it.match() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethod(
|
fun BytecodePatchContext.firstMethod(
|
||||||
vararg strings: String,
|
vararg strings: String,
|
||||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||||
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
|
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethodMutableOrNull(
|
fun BytecodePatchContext.firstMethodMutableOrNull(
|
||||||
vararg strings: String,
|
vararg strings: String,
|
||||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||||
) = with(predicate) {
|
) = with(MatchContext()) {
|
||||||
with(MatchContext()) {
|
|
||||||
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.let { methods ->
|
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.let { methods ->
|
||||||
methods.firstOrNull { it.match() }?.let { method ->
|
methods.firstOrNull { it.predicate() }?.let { method ->
|
||||||
firstClassDefMutable(method.definingClass).methods.first {
|
firstClassDefMutable(method.definingClass).methods.first {
|
||||||
MethodUtil.methodSignaturesMatch(
|
MethodUtil.methodSignaturesMatch(
|
||||||
method, it
|
method, it
|
||||||
@@ -152,10 +146,9 @@ fun BytecodePatchContext.firstMethodMutableOrNull(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun BytecodePatchContext.firstMethodMutable(
|
fun BytecodePatchContext.firstMethodMutable(
|
||||||
vararg strings: String, predicate: MatchPredicate<Method> = MatchPredicate { true }
|
vararg strings: String, predicate: context(MatchContext) Method.() -> Boolean = { true }
|
||||||
) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate))
|
) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate))
|
||||||
|
|
||||||
inline fun <reified C, T> ReadOnlyProperty(crossinline block: C.(KProperty<*>) -> T) =
|
inline fun <reified C, T> ReadOnlyProperty(crossinline block: C.(KProperty<*>) -> T) =
|
||||||
@@ -165,67 +158,82 @@ inline fun <reified C, T> ReadOnlyProperty(crossinline block: C.(KProperty<*>) -
|
|||||||
thisRef.block(property)
|
thisRef.block(property)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun gettingFirstClassDefOrNull(predicate: MatchPredicate<ClassDef>) =
|
fun gettingFirstClassDefOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefOrNull(predicate) }
|
ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefOrNull(predicate) }
|
||||||
|
|
||||||
fun gettingFirstClassDef(predicate: MatchPredicate<ClassDef>) = requireNotNull(gettingFirstClassDefOrNull(predicate))
|
fun gettingFirstClassDef(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
|
requireNotNull(gettingFirstClassDefOrNull(predicate))
|
||||||
|
|
||||||
fun gettingFirstClassDefMutableOrNull(predicate: MatchPredicate<ClassDef>) =
|
fun gettingFirstClassDefMutableOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefMutableOrNull(predicate) }
|
ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefMutableOrNull(predicate) }
|
||||||
|
|
||||||
fun gettingFirstClassDefMutable(predicate: MatchPredicate<ClassDef>) =
|
fun gettingFirstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||||
requireNotNull(gettingFirstClassDefMutableOrNull(predicate))
|
requireNotNull(gettingFirstClassDefMutableOrNull(predicate))
|
||||||
|
|
||||||
fun gettingFirstClassDefOrNull(
|
fun gettingFirstClassDefOrNull(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefOrNull(type, predicate) }
|
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefOrNull(type, predicate) }
|
||||||
|
|
||||||
fun gettingFirstClassDef(
|
fun gettingFirstClassDef(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = requireNotNull(gettingFirstClassDefOrNull(type, predicate))
|
) = requireNotNull(gettingFirstClassDefOrNull(type, predicate))
|
||||||
|
|
||||||
fun gettingFirstClassDefMutableOrNull(
|
fun gettingFirstClassDefMutableOrNull(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefMutableOrNull(type, predicate) }
|
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefMutableOrNull(type, predicate) }
|
||||||
|
|
||||||
fun gettingFirstClassDefMutable(
|
fun gettingFirstClassDefMutable(
|
||||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||||
) = requireNotNull(gettingFirstClassDefMutableOrNull(type, predicate))
|
) = requireNotNull(gettingFirstClassDefMutableOrNull(type, predicate))
|
||||||
|
|
||||||
fun gettingFirstMethodOrNull(predicate: MatchPredicate<Method>) =
|
fun gettingFirstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodOrNull(predicate) }
|
ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodOrNull(predicate) }
|
||||||
|
|
||||||
fun gettingFirstMethod(predicate: MatchPredicate<Method>) = requireNotNull(gettingFirstMethodOrNull(predicate))
|
fun gettingFirstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
|
requireNotNull(gettingFirstMethodOrNull(predicate))
|
||||||
|
|
||||||
fun gettingFirstMethodMutableOrNull(predicate: MatchPredicate<Method>) =
|
fun gettingFirstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodMutableOrNull(predicate) }
|
ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodMutableOrNull(predicate) }
|
||||||
|
|
||||||
fun gettingFirstMethodMutable(predicate: MatchPredicate<Method>) =
|
fun gettingFirstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||||
requireNotNull(gettingFirstMethodMutableOrNull(predicate))
|
requireNotNull(gettingFirstMethodMutableOrNull(predicate))
|
||||||
|
|
||||||
fun gettingFirstMethodOrNull(
|
fun gettingFirstMethodOrNull(
|
||||||
vararg strings: String,
|
vararg strings: String,
|
||||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||||
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodOrNull(*strings, predicate = predicate) }
|
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodOrNull(*strings, predicate = predicate) }
|
||||||
|
|
||||||
fun gettingFirstMethod(
|
fun gettingFirstMethod(
|
||||||
vararg strings: String,
|
vararg strings: String,
|
||||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||||
) = requireNotNull(gettingFirstMethodOrNull(*strings, predicate = predicate))
|
) = requireNotNull(gettingFirstMethodOrNull(*strings, predicate = predicate))
|
||||||
|
|
||||||
fun gettingFirstMethodMutableOrNull(
|
fun gettingFirstMethodMutableOrNull(
|
||||||
vararg strings: String,
|
vararg strings: String,
|
||||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||||
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodMutableOrNull(*strings, predicate = predicate) }
|
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodMutableOrNull(*strings, predicate = predicate) }
|
||||||
|
|
||||||
fun gettingFirstMethodMutable(
|
fun gettingFirstMethodMutable(
|
||||||
vararg strings: String,
|
vararg strings: String,
|
||||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||||
) = requireNotNull(gettingFirstMethodMutableOrNull(*strings, predicate = predicate))
|
) = requireNotNull(gettingFirstMethodMutableOrNull(*strings, predicate = predicate))
|
||||||
|
|
||||||
fun interface MatchPredicate<T> {
|
fun <T> indexedMatcher() = IndexedMatcher<T>()
|
||||||
context(MatchContext) fun T.match(): Boolean
|
|
||||||
}
|
// Add lambda to emit instructions if matched (or matched arg)
|
||||||
|
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
|
||||||
|
IndexedMatcher<T>().apply(build)
|
||||||
|
|
||||||
|
fun <T> Iterable<T>.matchIndexed(build: IndexedMatcher<T>.() -> Unit) =
|
||||||
|
indexedMatcher(build)(this)
|
||||||
|
|
||||||
|
context(_: MatchContext)
|
||||||
|
operator fun <T> IndexedMatcher<T>.invoke(iterable: Iterable<T>, key: String, builder: IndexedMatcher<T>.() -> Unit) =
|
||||||
|
remember(key) { IndexedMatcher<T>().apply(builder) }(iterable)
|
||||||
|
|
||||||
|
context(_: MatchContext)
|
||||||
|
fun <T> Iterable<T>.matchIndexed(key: String, build: IndexedMatcher<T>.() -> Unit) =
|
||||||
|
indexedMatcher<T>()(this, key, build)
|
||||||
|
|
||||||
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
|
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
|
||||||
var matchIndex = -1
|
var matchIndex = -1
|
||||||
@@ -234,101 +242,12 @@ abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
|
|||||||
abstract operator fun invoke(haystack: Iterable<T>): Boolean
|
abstract operator fun invoke(haystack: Iterable<T>): Boolean
|
||||||
|
|
||||||
class MatchContext internal constructor() : MutableMap<String, Any> by mutableMapOf() {
|
class MatchContext internal constructor() : MutableMap<String, Any> by mutableMapOf() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context(context: MatchContext)
|
||||||
inline fun <reified V : Any> remember(key: String, defaultValue: () -> V) =
|
inline fun <reified V : Any> remember(key: String, defaultValue: () -> V) =
|
||||||
get(key) as? V ?: defaultValue().also { put(key, it) }
|
context[key] as? V ?: defaultValue().also { context[key] = it }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> slidingWindowMatcher(build: SlidingWindowMatcher<T>.() -> Unit) =
|
|
||||||
SlidingWindowMatcher<T>().apply(build)
|
|
||||||
|
|
||||||
context(MatchContext)
|
|
||||||
fun <T> Iterable<T>.matchSlidingWindow(key: String, build: SlidingWindowMatcher<T>.() -> Unit) =
|
|
||||||
remember(key) { slidingWindowMatcher(build) }(this)
|
|
||||||
|
|
||||||
fun <T> Iterable<T>.matchSlidingWindow(build: SlidingWindowMatcher<T>.() -> Unit) =
|
|
||||||
slidingWindowMatcher(build)(this)
|
|
||||||
|
|
||||||
class SlidingWindowMatcher<T>() : Matcher<T, T.() -> Boolean>() {
|
|
||||||
override operator fun invoke(haystack: Iterable<T>): Boolean {
|
|
||||||
val haystackCount = haystack.count()
|
|
||||||
val needleSize = size
|
|
||||||
if (needleSize == 0) return false
|
|
||||||
|
|
||||||
for (i in 0..(haystackCount - needleSize)) {
|
|
||||||
var matched = true
|
|
||||||
for (j in 0 until needleSize) {
|
|
||||||
if (!this[j].invoke(haystack.elementAt(i + j))) {
|
|
||||||
matched = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (matched) {
|
|
||||||
matchIndex = i
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matchIndex = -1
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findStringsMatcher(build: MutableList<String>.() -> Unit) =
|
|
||||||
FindStringsMatcher().apply(build)
|
|
||||||
|
|
||||||
class FindStringsMatcher() : Matcher<Instruction, String>() {
|
|
||||||
private val _matchedStrings = mutableListOf<Pair<String, Int>>()
|
|
||||||
val matchedStrings: List<Pair<String, Int>> = _matchedStrings
|
|
||||||
|
|
||||||
override fun invoke(haystack: Iterable<Instruction>): Boolean {
|
|
||||||
_matchedStrings.clear()
|
|
||||||
val remaining = indices.toMutableList()
|
|
||||||
|
|
||||||
haystack.forEachIndexed { hayIndex, instruction ->
|
|
||||||
val string = ((instruction as? ReferenceInstruction)?.reference as? StringReference)?.string
|
|
||||||
?: return@forEachIndexed
|
|
||||||
|
|
||||||
val index = remaining.firstOrNull { this[it] in string } ?: return@forEachIndexed
|
|
||||||
|
|
||||||
_matchedStrings += this[index] to hayIndex
|
|
||||||
remaining -= index
|
|
||||||
}
|
|
||||||
|
|
||||||
return remaining.isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun BytecodePatchContext.a() {
|
|
||||||
val match = indexedMatcher<Instruction> {
|
|
||||||
first { opcode == Opcode.OR_INT_2ADDR }
|
|
||||||
after { opcode == Opcode.RETURN_VOID }
|
|
||||||
after(atLeast = 2, atMost = 5) { opcode == Opcode.MOVE_RESULT_OBJECT }
|
|
||||||
opcode(Opcode.RETURN_VOID)
|
|
||||||
}
|
|
||||||
|
|
||||||
val myMethod = firstMethod {
|
|
||||||
implementation { match(instructions) }
|
|
||||||
}
|
|
||||||
|
|
||||||
match._indices // Mapped in same order as defined
|
|
||||||
}
|
|
||||||
|
|
||||||
fun IndexedMatcher<Instruction>.opcode(opcode: Opcode) {
|
|
||||||
after { this.opcode == opcode }
|
|
||||||
}
|
|
||||||
|
|
||||||
context(MatchContext)
|
|
||||||
fun Method.instructions(key: String, build: IndexedMatcher<Instruction>.() -> Unit) =
|
|
||||||
instructions.matchIndexed("instructions", build)
|
|
||||||
|
|
||||||
|
|
||||||
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
|
|
||||||
IndexedMatcher<T>().apply(build)
|
|
||||||
|
|
||||||
context(MatchContext)
|
|
||||||
fun <T> Iterable<T>.matchIndexed(key: String, build: IndexedMatcher<T>.() -> Unit) =
|
|
||||||
remember<IndexedMatcher<T>>(key) { indexedMatcher(build) }(this)
|
|
||||||
|
|
||||||
class IndexedMatcher<T>() : Matcher<T, T.() -> Boolean>() {
|
class IndexedMatcher<T>() : Matcher<T, T.() -> Boolean>() {
|
||||||
private val _indices: MutableList<Int> = mutableListOf()
|
private val _indices: MutableList<Int> = mutableListOf()
|
||||||
@@ -395,3 +314,62 @@ class IndexedMatcher<T>() : Matcher<T, T.() -> Boolean>() {
|
|||||||
if (distance in atLeast..atMost) predicate() else false
|
if (distance in atLeast..atMost) predicate() else false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun BytecodePatchContext.matchers() {
|
||||||
|
val customMatcher = object : Matcher<Instruction, Instruction.() -> Boolean>() {
|
||||||
|
override fun invoke(haystack: Iterable<Instruction>) = true
|
||||||
|
}.apply { add { true } }
|
||||||
|
|
||||||
|
// Probably gonna make this ctor internal.
|
||||||
|
IndexedMatcher<Instruction>().apply { add { true } }
|
||||||
|
// Since there is a function for it.
|
||||||
|
val m = indexedMatcher<Instruction>()
|
||||||
|
m.apply { add { true } }
|
||||||
|
|
||||||
|
// You can directly use the extension function to match.
|
||||||
|
listOf<Instruction>().matchIndexed { add { true } }
|
||||||
|
|
||||||
|
// Inside a match context extension functions are cacheable:
|
||||||
|
firstMethod { instructions.matchIndexed("key") { add { true } } }
|
||||||
|
|
||||||
|
// Or create a matcher outside a context and use it in a MatchContext as follows:
|
||||||
|
val match = indexedMatcher<Instruction>()
|
||||||
|
firstMethod { match(instructions, "anotherKey") { first { opcode(Opcode.RETURN_VOID) } } }
|
||||||
|
match.indices // so that you can access the matched indices later.
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BytecodePatchContext.a() {
|
||||||
|
val matcher = indexedMatcher<Instruction>()
|
||||||
|
|
||||||
|
firstMethodMutableOrNull("string to find in lookupmap") {
|
||||||
|
returnType == "L" && matcher(instructions, "key") {
|
||||||
|
first {
|
||||||
|
// The first instruction is a field reference to a field in the class of the method being matched.
|
||||||
|
// We cache the field name using remember to avoid redundant lookups.
|
||||||
|
fieldReference!!.name == remember("fieldName") {
|
||||||
|
firstClassDef(definingClass).fields.first().name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
after { fieldReference("fieldName") }
|
||||||
|
after {
|
||||||
|
opcode(Opcode.NEW_INSTANCE) && methodReference { toString() == "Lcom/example/MyClass;" }
|
||||||
|
}
|
||||||
|
// Followed by 2 to 4 string instructions starting with "test".
|
||||||
|
after(atLeast = 1, atMost = 2) { string { startsWith("test") } }
|
||||||
|
} && parameterTypes.matchIndexed("params") {
|
||||||
|
// Fully dynamic environment to customize depending on your needs.
|
||||||
|
operator fun String.plus(other: String) {
|
||||||
|
after { this == this@plus }
|
||||||
|
after { this == other }
|
||||||
|
}
|
||||||
|
|
||||||
|
"L" + "I" + "Z" // Matches parameter types "L", "I", "Z" in order.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstMethodMutable {
|
||||||
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) && instructions.matchIndexed("anotherKey") {
|
||||||
|
first { opcode(Opcode.RETURN_VOID) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.patcher.dex.mutable
|
package app.revanced.patcher.dex.mutable
|
||||||
|
|
||||||
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue
|
|
||||||
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
|
|
||||||
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue
|
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue
|
||||||
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
|
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
|
||||||
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
|
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package app.revanced.patcher.extensions
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.DualReferenceInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.DualReferenceInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
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.instruction.ReferenceInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.Reference
|
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.*
|
||||||
|
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
|
fun <T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
|
||||||
@@ -12,3 +15,52 @@ fun <T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
|
|||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun <T : Reference> Instruction.reference2(predicate: T.() -> Boolean) =
|
fun <T : Reference> Instruction.reference2(predicate: T.() -> Boolean) =
|
||||||
((this as? DualReferenceInstruction)?.reference2 as? T)?.predicate() ?: false
|
((this as? DualReferenceInstruction)?.reference2 as? T)?.predicate() ?: false
|
||||||
|
|
||||||
|
fun Instruction.methodReference(predicate: MethodReference.() -> Boolean) =
|
||||||
|
reference(predicate)
|
||||||
|
|
||||||
|
fun Instruction.methodReference(methodReference: MethodReference) =
|
||||||
|
methodReference { MethodUtil.methodSignaturesMatch(methodReference, this) }
|
||||||
|
|
||||||
|
fun Instruction.fieldReference(predicate: FieldReference.() -> Boolean) =
|
||||||
|
reference(predicate)
|
||||||
|
|
||||||
|
fun Instruction.fieldReference(fieldName: String) =
|
||||||
|
fieldReference { name == fieldName }
|
||||||
|
|
||||||
|
fun Instruction.type(predicate: String.() -> Boolean) =
|
||||||
|
reference<TypeReference> { type.predicate() }
|
||||||
|
|
||||||
|
fun Instruction.type(type: String) =
|
||||||
|
type { this == type }
|
||||||
|
|
||||||
|
fun Instruction.string(predicate: String.() -> Boolean) =
|
||||||
|
reference<StringReference> { string.predicate() }
|
||||||
|
|
||||||
|
fun Instruction.string(string: String) =
|
||||||
|
string { this == string }
|
||||||
|
|
||||||
|
fun Instruction.opcode(opcode: Opcode) = this.opcode == opcode
|
||||||
|
|
||||||
|
fun Instruction.wideLiteral(wideLiteral: Long) = (this as? WideLiteralInstruction)?.wideLiteral == wideLiteral
|
||||||
|
|
||||||
|
private inline fun <reified T : Reference> Instruction.reference(): T? =
|
||||||
|
(this as? ReferenceInstruction)?.reference as? T
|
||||||
|
|
||||||
|
val Instruction.reference: Reference?
|
||||||
|
get() = reference()
|
||||||
|
|
||||||
|
val Instruction.methodReference get() =
|
||||||
|
reference<MethodReference>()
|
||||||
|
|
||||||
|
val Instruction.fieldReference get() =
|
||||||
|
reference<FieldReference>()
|
||||||
|
|
||||||
|
val Instruction.typeReference get() =
|
||||||
|
reference<TypeReference>()
|
||||||
|
|
||||||
|
val Instruction.stringReference get() =
|
||||||
|
reference<StringReference>()
|
||||||
|
|
||||||
|
val Instruction.wideLiteral get() =
|
||||||
|
(this as? WideLiteralInstruction)?.wideLiteral
|
||||||
|
|||||||
@@ -1,23 +1,19 @@
|
|||||||
package app.revanced.patcher.extensions
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
import app.revanced.patcher.dex.mutable.MutableMethod
|
import app.revanced.patcher.dex.mutable.MutableMethod
|
||||||
import app.revanced.patcher.util.smali.ExternalLabel
|
import app.revanced.patcher.util.toInstructions
|
||||||
import app.revanced.patcher.util.smali.toInstructions
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
||||||
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||||
import com.android.tools.smali.dexlib2.builder.Label
|
import com.android.tools.smali.dexlib2.builder.Label
|
||||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10t
|
import com.android.tools.smali.dexlib2.builder.instruction.*
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction20t
|
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
|
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22t
|
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction30t
|
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction31t
|
|
||||||
import com.android.tools.smali.dexlib2.iface.Method
|
import com.android.tools.smali.dexlib2.iface.Method
|
||||||
import com.android.tools.smali.dexlib2.iface.MethodImplementation
|
import com.android.tools.smali.dexlib2.iface.MethodImplementation
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||||
import kotlin.collections.forEach
|
|
||||||
import kotlin.collections.indexOf
|
fun Method.accessFlags(vararg flags: AccessFlags) =
|
||||||
|
accessFlags.and(flags.map { it.ordinal }.reduce { acc, i -> acc or i }) != 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add instructions to a method at the given index.
|
* Add instructions to a method at the given index.
|
||||||
@@ -382,3 +378,10 @@ val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instruct
|
|||||||
* @return The label.
|
* @return The label.
|
||||||
*/
|
*/
|
||||||
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
|
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that represents a label for an instruction.
|
||||||
|
* @param name The label name.
|
||||||
|
* @param instruction The instruction that this label is for.
|
||||||
|
*/
|
||||||
|
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
package app.revanced.patcher.util
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
import app.revanced.patcher.patch.BytecodePatchContext
|
|
||||||
import app.revanced.patcher.dex.mutable.MutableMethod
|
import app.revanced.patcher.dex.mutable.MutableMethod
|
||||||
import app.revanced.patcher.extensions.instructionsOrNull
|
import app.revanced.patcher.extensions.instructionsOrNull
|
||||||
|
import app.revanced.patcher.patch.BytecodePatchContext
|
||||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||||
import com.android.tools.smali.dexlib2.iface.Method
|
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.Instruction
|
||||||
@@ -23,12 +23,12 @@ import kotlin.reflect.KProperty
|
|||||||
* @throws NavigateException If the method does not have an implementation.
|
* @throws NavigateException If the method does not have an implementation.
|
||||||
* @throws NavigateException If the instruction at the specified index is not a method reference.
|
* @throws NavigateException If the instruction at the specified index is not a method reference.
|
||||||
*/
|
*/
|
||||||
context(BytecodePatchContext)
|
|
||||||
class MethodNavigator internal constructor(
|
class MethodNavigator internal constructor(
|
||||||
private var startMethod: MethodReference,
|
private var startMethod: MethodReference,
|
||||||
) {
|
) {
|
||||||
private var lastNavigatedMethodReference = startMethod
|
private var lastNavigatedMethodReference = startMethod
|
||||||
|
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
private val lastNavigatedMethodInstructions
|
private val lastNavigatedMethodInstructions
|
||||||
get() = with(original()) {
|
get() = with(original()) {
|
||||||
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
|
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
|
||||||
@@ -41,6 +41,7 @@ class MethodNavigator internal constructor(
|
|||||||
*
|
*
|
||||||
* @return This [MethodNavigator].
|
* @return This [MethodNavigator].
|
||||||
*/
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
fun to(vararg index: Int): MethodNavigator {
|
fun to(vararg index: Int): MethodNavigator {
|
||||||
index.forEach {
|
index.forEach {
|
||||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
|
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
|
||||||
@@ -55,6 +56,7 @@ class MethodNavigator internal constructor(
|
|||||||
* @param index The index of the method to navigate to.
|
* @param index The index of the method to navigate to.
|
||||||
* @param predicate The predicate to match.
|
* @param predicate The predicate to match.
|
||||||
*/
|
*/
|
||||||
|
context(_: BytecodePatchContext)
|
||||||
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
|
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
|
||||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
|
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
|
||||||
.filter(predicate).asIterable().getMethodReferenceAt(index)
|
.filter(predicate).asIterable().getMethodReferenceAt(index)
|
||||||
@@ -79,22 +81,27 @@ class MethodNavigator internal constructor(
|
|||||||
*
|
*
|
||||||
* @return The last navigated method mutably.
|
* @return The last navigated method mutably.
|
||||||
*/
|
*/
|
||||||
fun stop() = classDefs.find(matchesCurrentMethodReferenceDefiningClass)!!.mutable().firstMethodBySignature
|
context(context: BytecodePatchContext)
|
||||||
|
fun stop() = with(context) {
|
||||||
|
classDefs.find(matchesCurrentMethodReferenceDefiningClass)!!.mutable().firstMethodBySignature
|
||||||
as MutableMethod
|
as MutableMethod
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last navigated method mutably.
|
* Get the last navigated method mutably.
|
||||||
*
|
*
|
||||||
* @return The last navigated method mutably.
|
* @return The last navigated method mutably.
|
||||||
*/
|
*/
|
||||||
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = stop()
|
operator fun getValue(context: BytecodePatchContext?, property: KProperty<*>) =
|
||||||
|
context(requireNotNull(context)) { stop() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last navigated method immutably.
|
* Get the last navigated method immutably.
|
||||||
*
|
*
|
||||||
* @return The last navigated method immutably.
|
* @return The last navigated method immutably.
|
||||||
*/
|
*/
|
||||||
fun original(): Method = classDefs.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
|
context(context: BytecodePatchContext)
|
||||||
|
fun original(): Method = context.classDefs.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Predicate to match the class defining the current method reference.
|
* Predicate to match the class defining the current method reference.
|
||||||
|
|||||||
66
src/main/kotlin/app/revanced/patcher/util/Smali.kt
Normal file
66
src/main/kotlin/app/revanced/patcher/util/Smali.kt
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import app.revanced.patcher.dex.mutable.MutableMethod
|
||||||
|
import app.revanced.patcher.extensions.instructions
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.Opcodes
|
||||||
|
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
|
||||||
|
import com.android.tools.smali.smali.smaliFlexLexer
|
||||||
|
import com.android.tools.smali.smali.smaliParser
|
||||||
|
import com.android.tools.smali.smali.smaliTreeWalker
|
||||||
|
import org.antlr.runtime.CommonTokenStream
|
||||||
|
import org.antlr.runtime.TokenSource
|
||||||
|
import org.antlr.runtime.tree.CommonTreeNodeStream
|
||||||
|
import java.io.StringReader
|
||||||
|
|
||||||
|
private const val CLASS_HEADER = ".class LInlineCompiler;\n.super Ljava/lang/Object;\n"
|
||||||
|
private const val STATIC_HEADER = "$CLASS_HEADER.method public static dummyMethod("
|
||||||
|
private const val HEADER = "$CLASS_HEADER.method public dummyMethod("
|
||||||
|
|
||||||
|
private val dexBuilder = DexBuilder(Opcodes.getDefault())
|
||||||
|
private val sb by lazy { StringBuilder(512) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile lines of Smali code to a list of instructions.
|
||||||
|
*
|
||||||
|
* Note: Adding compiled instructions to an existing method with
|
||||||
|
* offset instructions WITHOUT specifying a parent method will not work.
|
||||||
|
* @param templateMethod The method to compile the instructions against.
|
||||||
|
* @returns A list of instructions.
|
||||||
|
*/
|
||||||
|
fun String.toInstructions(templateMethod: MutableMethod? = null): List<BuilderInstruction> {
|
||||||
|
val parameters = templateMethod?.parameterTypes?.joinToString("") { it } ?: ""
|
||||||
|
val registers = templateMethod?.implementation?.registerCount ?: 1 // TODO: Should this be 0?
|
||||||
|
val isStatic = templateMethod?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true
|
||||||
|
|
||||||
|
sb.setLength(0) // reset
|
||||||
|
|
||||||
|
if (isStatic) sb.append(STATIC_HEADER) else sb.append(HEADER)
|
||||||
|
sb.append(parameters).append(")V\n")
|
||||||
|
sb.append(" .registers ").append(registers).append("\n")
|
||||||
|
sb.append(trimIndent()).append("\n")
|
||||||
|
sb.append(".end method")
|
||||||
|
|
||||||
|
val reader = StringReader(sb.toString())
|
||||||
|
val lexer = smaliFlexLexer(reader, 15)
|
||||||
|
val tokens = CommonTokenStream(lexer as TokenSource)
|
||||||
|
val parser = smaliParser(tokens)
|
||||||
|
val fileTree = parser.smali_file()
|
||||||
|
|
||||||
|
if (lexer.numberOfSyntaxErrors > 0 || parser.numberOfSyntaxErrors > 0) {
|
||||||
|
throw IllegalStateException(
|
||||||
|
"Lexer errors: ${lexer.numberOfSyntaxErrors}, Parser errors: ${parser.numberOfSyntaxErrors}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val treeStream = CommonTreeNodeStream(fileTree.tree).apply {
|
||||||
|
tokenStream = tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
val walker = smaliTreeWalker(treeStream)
|
||||||
|
walker.setDexBuilder(dexBuilder)
|
||||||
|
|
||||||
|
val classDef = walker.smali_file()
|
||||||
|
return classDef.methods.first().instructions.map { it as BuilderInstruction }
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package app.revanced.patcher.util.smali
|
|
||||||
|
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class that represents a label for an instruction.
|
|
||||||
* @param name The label name.
|
|
||||||
* @param instruction The instruction that this label is for.
|
|
||||||
*/
|
|
||||||
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package app.revanced.patcher.util.smali
|
|
||||||
|
|
||||||
import app.revanced.patcher.dex.mutable.MutableMethod
|
|
||||||
import app.revanced.patcher.extensions.instructions
|
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
|
||||||
import com.android.tools.smali.dexlib2.Opcodes
|
|
||||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
|
||||||
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
|
|
||||||
import com.android.tools.smali.smali.LexerErrorInterface
|
|
||||||
import com.android.tools.smali.smali.smaliFlexLexer
|
|
||||||
import com.android.tools.smali.smali.smaliParser
|
|
||||||
import com.android.tools.smali.smali.smaliTreeWalker
|
|
||||||
import org.antlr.runtime.CommonTokenStream
|
|
||||||
import org.antlr.runtime.TokenSource
|
|
||||||
import org.antlr.runtime.tree.CommonTreeNodeStream
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private const val METHOD_TEMPLATE = """
|
|
||||||
.class LInlineCompiler;
|
|
||||||
.super Ljava/lang/Object;
|
|
||||||
.method %s dummyMethod(%s)V
|
|
||||||
.registers %d
|
|
||||||
%s
|
|
||||||
.end method
|
|
||||||
"""
|
|
||||||
|
|
||||||
class InlineSmaliCompiler {
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Compiles a string of Smali code to a list of instructions.
|
|
||||||
* Special registers (such as p0, p1) will only work correctly
|
|
||||||
* if the parameters and registers of the method are passed.
|
|
||||||
*/
|
|
||||||
fun compile(
|
|
||||||
instructions: String,
|
|
||||||
parameters: String,
|
|
||||||
registers: Int,
|
|
||||||
forStaticMethod: Boolean,
|
|
||||||
): List<BuilderInstruction> {
|
|
||||||
val input =
|
|
||||||
METHOD_TEMPLATE.format(
|
|
||||||
Locale.ENGLISH,
|
|
||||||
if (forStaticMethod) {
|
|
||||||
"static"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
},
|
|
||||||
parameters,
|
|
||||||
registers,
|
|
||||||
instructions,
|
|
||||||
)
|
|
||||||
val reader = InputStreamReader(input.byteInputStream(), Charsets.UTF_8)
|
|
||||||
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
|
|
||||||
val tokens = CommonTokenStream(lexer as TokenSource)
|
|
||||||
val parser = smaliParser(tokens)
|
|
||||||
val result = parser.smali_file()
|
|
||||||
if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) {
|
|
||||||
throw IllegalStateException(
|
|
||||||
"Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val treeStream = CommonTreeNodeStream(result.tree)
|
|
||||||
treeStream.tokenStream = tokens
|
|
||||||
val dexGen = smaliTreeWalker(treeStream)
|
|
||||||
dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault()))
|
|
||||||
val classDef = dexGen.smali_file()
|
|
||||||
return classDef.methods.first().instructions.map { it as BuilderInstruction }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile lines of Smali code to a list of instructions.
|
|
||||||
*
|
|
||||||
* Note: Adding compiled instructions to an existing method with
|
|
||||||
* offset instructions WITHOUT specifying a parent method will not work.
|
|
||||||
* @param method The method to compile the instructions against.
|
|
||||||
* @returns A list of instructions.
|
|
||||||
*/
|
|
||||||
fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> {
|
|
||||||
return InlineSmaliCompiler.compile(
|
|
||||||
this,
|
|
||||||
method?.parameters?.joinToString("") { it } ?: "",
|
|
||||||
method?.implementation?.registerCount ?: 1,
|
|
||||||
method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile a line of Smali code to an instruction.
|
|
||||||
* @param templateMethod The method to compile the instructions against.
|
|
||||||
* @return The instruction.
|
|
||||||
*/
|
|
||||||
fun String.toInstruction(templateMethod: MutableMethod? = null) = this.toInstructions(templateMethod).first()
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package app.revanced.patcher.util.smali
|
package app.revanced.patcher.util.smali
|
||||||
|
|
||||||
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
|
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
|
||||||
import app.revanced.patcher.extensions.addInstructions
|
import app.revanced.patcher.extensions.*
|
||||||
import app.revanced.patcher.extensions.addInstructionsWithLabels
|
import app.revanced.patcher.util.toInstructions
|
||||||
import app.revanced.patcher.extensions.getInstruction
|
|
||||||
import app.revanced.patcher.extensions.newLabel
|
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
||||||
@@ -22,7 +20,7 @@ internal object InlineSmaliCompilerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `outputs valid instruction`() {
|
fun `outputs valid instruction`() {
|
||||||
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
|
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
|
||||||
val have = "const-string v0, \"Test\"".toInstruction()
|
val have = "const-string v0, \"Test\"".toInstructions().first()
|
||||||
|
|
||||||
assertInstructionsEqual(want, have)
|
assertInstructionsEqual(want, have)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user