mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-10 21:36: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 {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
|
||||
freeCompilerArgs = listOf("-Xcontext-receivers")
|
||||
freeCompilerArgs = listOf("-Xcontext-parameters")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
org.gradle.parallel = 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.dex.mutable.MutableMethod
|
||||
import app.revanced.patcher.extensions.instructions
|
||||
import app.revanced.patcher.extensions.*
|
||||
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.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.*
|
||||
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.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
@@ -52,110 +51,104 @@ fun MethodImplementation.anyTryBlock(predicate: TryBlock<out ExceptionHandler>.(
|
||||
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.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>) =
|
||||
with(predicate) { with(MatchContext()) { classDefs.firstOrNull { it.match() } } }
|
||||
|
||||
fun BytecodePatchContext.firstClassDef(predicate: MatchPredicate<ClassDef>) =
|
||||
fun BytecodePatchContext.firstClassDef(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||
requireNotNull(firstClassDefOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: MatchPredicate<ClassDef>) =
|
||||
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||
firstClassDefOrNull(predicate)?.mutable()
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutable(predicate: MatchPredicate<ClassDef>) =
|
||||
fun BytecodePatchContext.firstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||
requireNotNull(firstClassDefMutableOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstClassDefOrNull(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = lookupMaps.classDefsByType[type]?.takeIf {
|
||||
predicate == null || with(predicate) { with(MatchContext()) { it.match() } }
|
||||
predicate == null || with(MatchContext()) { it.predicate() }
|
||||
}
|
||||
|
||||
fun BytecodePatchContext.firstClassDef(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = requireNotNull(firstClassDefOrNull(type, predicate))
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutableOrNull(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = firstClassDefOrNull(type, predicate)?.mutable()
|
||||
|
||||
fun BytecodePatchContext.firstClassDefMutable(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = requireNotNull(firstClassDefMutableOrNull(type, predicate))
|
||||
|
||||
fun Iterable<ClassDef>.firstMethodOrNull(predicate: MatchPredicate<Method>) = with(predicate) {
|
||||
fun Iterable<ClassDef>.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
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)
|
||||
fun Iterable<ClassDef>.firstMethodMutableOrNull(predicate: MatchPredicate<Method>): MutableMethod? {
|
||||
with(predicate) {
|
||||
context(context: BytecodePatchContext)
|
||||
fun Iterable<ClassDef>.firstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean): MutableMethod? =
|
||||
with(context) {
|
||||
with(MatchContext()) {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
context(BytecodePatchContext)
|
||||
fun Iterable<ClassDef>.firstMethodMutable(predicate: MatchPredicate<Method>) =
|
||||
context(_: BytecodePatchContext)
|
||||
fun Iterable<ClassDef>.firstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
requireNotNull(firstMethodMutableOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodOrNull(predicate: MatchPredicate<Method>) =
|
||||
fun BytecodePatchContext.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
classDefs.firstMethodOrNull(predicate)
|
||||
|
||||
fun BytecodePatchContext.firstMethod(predicate: MatchPredicate<Method>) =
|
||||
fun BytecodePatchContext.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
requireNotNull(firstMethodOrNull(predicate))
|
||||
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: MatchPredicate<Method>) =
|
||||
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
classDefs.firstMethodMutableOrNull(predicate)
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutable(predicate: MatchPredicate<Method>) =
|
||||
fun BytecodePatchContext.firstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
requireNotNull(firstMethodMutableOrNull(predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodOrNull(
|
||||
vararg strings: String,
|
||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
||||
) = with(predicate) {
|
||||
with(MatchContext()) {
|
||||
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.firstOrNull { it.match() }
|
||||
}
|
||||
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||
) = with(MatchContext()) {
|
||||
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.firstOrNull { it.predicate() }
|
||||
}
|
||||
|
||||
fun BytecodePatchContext.firstMethod(
|
||||
vararg strings: String,
|
||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
||||
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
|
||||
|
||||
fun BytecodePatchContext.firstMethodMutableOrNull(
|
||||
vararg strings: String,
|
||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
||||
) = with(predicate) {
|
||||
with(MatchContext()) {
|
||||
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.let { methods ->
|
||||
methods.firstOrNull { it.match() }?.let { method ->
|
||||
firstClassDefMutable(method.definingClass).methods.first {
|
||||
MethodUtil.methodSignaturesMatch(
|
||||
method, it
|
||||
)
|
||||
}
|
||||
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||
) = with(MatchContext()) {
|
||||
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.let { methods ->
|
||||
methods.firstOrNull { it.predicate() }?.let { method ->
|
||||
firstClassDefMutable(method.definingClass).methods.first {
|
||||
MethodUtil.methodSignaturesMatch(
|
||||
method, it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun gettingFirstClassDefOrNull(predicate: MatchPredicate<ClassDef>) =
|
||||
fun gettingFirstClassDefOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||
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) }
|
||||
|
||||
fun gettingFirstClassDefMutable(predicate: MatchPredicate<ClassDef>) =
|
||||
fun gettingFirstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
|
||||
requireNotNull(gettingFirstClassDefMutableOrNull(predicate))
|
||||
|
||||
fun gettingFirstClassDefOrNull(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefOrNull(type, predicate) }
|
||||
|
||||
fun gettingFirstClassDef(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = requireNotNull(gettingFirstClassDefOrNull(type, predicate))
|
||||
|
||||
fun gettingFirstClassDefMutableOrNull(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefMutableOrNull(type, predicate) }
|
||||
|
||||
fun gettingFirstClassDefMutable(
|
||||
type: String, predicate: (MatchPredicate<ClassDef>)? = null
|
||||
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
|
||||
) = requireNotNull(gettingFirstClassDefMutableOrNull(type, predicate))
|
||||
|
||||
fun gettingFirstMethodOrNull(predicate: MatchPredicate<Method>) =
|
||||
fun gettingFirstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
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) }
|
||||
|
||||
fun gettingFirstMethodMutable(predicate: MatchPredicate<Method>) =
|
||||
fun gettingFirstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
|
||||
requireNotNull(gettingFirstMethodMutableOrNull(predicate))
|
||||
|
||||
fun gettingFirstMethodOrNull(
|
||||
vararg strings: String,
|
||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
||||
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodOrNull(*strings, predicate = predicate) }
|
||||
|
||||
fun gettingFirstMethod(
|
||||
vararg strings: String,
|
||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
||||
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||
) = requireNotNull(gettingFirstMethodOrNull(*strings, predicate = predicate))
|
||||
|
||||
fun gettingFirstMethodMutableOrNull(
|
||||
vararg strings: String,
|
||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
||||
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodMutableOrNull(*strings, predicate = predicate) }
|
||||
|
||||
fun gettingFirstMethodMutable(
|
||||
vararg strings: String,
|
||||
predicate: MatchPredicate<Method> = MatchPredicate { true },
|
||||
predicate: context(MatchContext) Method.() -> Boolean = { true },
|
||||
) = requireNotNull(gettingFirstMethodMutableOrNull(*strings, predicate = predicate))
|
||||
|
||||
fun interface MatchPredicate<T> {
|
||||
context(MatchContext) fun T.match(): Boolean
|
||||
}
|
||||
fun <T> indexedMatcher() = IndexedMatcher<T>()
|
||||
|
||||
// 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() {
|
||||
var matchIndex = -1
|
||||
@@ -234,101 +242,12 @@ abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
|
||||
abstract operator fun invoke(haystack: Iterable<T>): Boolean
|
||||
|
||||
class MatchContext internal constructor() : MutableMap<String, Any> by mutableMapOf() {
|
||||
inline fun <reified V : Any> remember(key: String, defaultValue: () -> V) =
|
||||
get(key) as? V ?: defaultValue().also { put(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)
|
||||
context(context: MatchContext)
|
||||
inline fun <reified V : Any> remember(key: String, defaultValue: () -> V) =
|
||||
context[key] as? V ?: defaultValue().also { context[key] = it }
|
||||
|
||||
class IndexedMatcher<T>() : Matcher<T, T.() -> Boolean>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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.Instruction
|
||||
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")
|
||||
fun <T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
|
||||
@@ -12,3 +15,52 @@ fun <T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Reference> Instruction.reference2(predicate: T.() -> Boolean) =
|
||||
((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
|
||||
|
||||
import app.revanced.patcher.dex.mutable.MutableMethod
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.patcher.util.smali.toInstructions
|
||||
import app.revanced.patcher.util.toInstructions
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||
import com.android.tools.smali.dexlib2.builder.Label
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10t
|
||||
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.builder.instruction.*
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.MethodImplementation
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
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.
|
||||
@@ -382,3 +378,10 @@ val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instruct
|
||||
* @return The label.
|
||||
*/
|
||||
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
|
||||
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.dex.mutable.MutableMethod
|
||||
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.Method
|
||||
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 instruction at the specified index is not a method reference.
|
||||
*/
|
||||
context(BytecodePatchContext)
|
||||
class MethodNavigator internal constructor(
|
||||
private var startMethod: MethodReference,
|
||||
) {
|
||||
private var lastNavigatedMethodReference = startMethod
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
private val lastNavigatedMethodInstructions
|
||||
get() = with(original()) {
|
||||
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
|
||||
@@ -41,6 +41,7 @@ class MethodNavigator internal constructor(
|
||||
*
|
||||
* @return This [MethodNavigator].
|
||||
*/
|
||||
context(_: BytecodePatchContext)
|
||||
fun to(vararg index: Int): MethodNavigator {
|
||||
index.forEach {
|
||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
|
||||
@@ -55,6 +56,7 @@ class MethodNavigator internal constructor(
|
||||
* @param index The index of the method to navigate to.
|
||||
* @param predicate The predicate to match.
|
||||
*/
|
||||
context(_: BytecodePatchContext)
|
||||
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
|
||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
|
||||
.filter(predicate).asIterable().getMethodReferenceAt(index)
|
||||
@@ -79,22 +81,27 @@ class MethodNavigator internal constructor(
|
||||
*
|
||||
* @return The last navigated method mutably.
|
||||
*/
|
||||
fun stop() = classDefs.find(matchesCurrentMethodReferenceDefiningClass)!!.mutable().firstMethodBySignature
|
||||
as MutableMethod
|
||||
context(context: BytecodePatchContext)
|
||||
fun stop() = with(context) {
|
||||
classDefs.find(matchesCurrentMethodReferenceDefiningClass)!!.mutable().firstMethodBySignature
|
||||
as MutableMethod
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 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.
|
||||
*
|
||||
* @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.
|
||||
|
||||
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
|
||||
|
||||
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patcher.extensions.addInstructions
|
||||
import app.revanced.patcher.extensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.getInstruction
|
||||
import app.revanced.patcher.extensions.newLabel
|
||||
import app.revanced.patcher.extensions.*
|
||||
import app.revanced.patcher.util.toInstructions
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
||||
@@ -22,7 +20,7 @@ internal object InlineSmaliCompilerTest {
|
||||
@Test
|
||||
fun `outputs valid instruction`() {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user