mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-11 13:56:16 +00:00
Modernize patch api names, deprecate fingerprints, simplify patching code even more, add mutablemethod implementation setter, refactor tests and improve for better coverage
This commit is contained in:
@@ -15,7 +15,7 @@ import kotlin.test.assertNotNull
|
|||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
object MatchingTest : PatcherTestBase() {
|
object MatchingTest : PatcherTestBase() {
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
fun setUp() = setUpMock()
|
fun setup() = setupMock()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `matches via builder api`() {
|
fun `matches via builder api`() {
|
||||||
@@ -37,7 +37,7 @@ object MatchingTest : PatcherTestBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
execute {
|
apply {
|
||||||
assertNotNull(firstMethodBuilder().methodOrNull) { "Expected to find a method" }
|
assertNotNull(firstMethodBuilder().methodOrNull) { "Expected to find a method" }
|
||||||
Assertions.assertNull(firstMethodBuilder(fail = true).methodOrNull) { "Expected to not find a method" }
|
Assertions.assertNull(firstMethodBuilder(fail = true).methodOrNull) { "Expected to not find a method" }
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ object MatchingTest : PatcherTestBase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `matches via declarative api`() {
|
fun `matches via declarative api`() {
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
execute {
|
apply {
|
||||||
val method = firstMethodByDeclarativePredicateOrNull {
|
val method = firstMethodByDeclarativePredicateOrNull {
|
||||||
anyOf {
|
anyOf {
|
||||||
predicate { name == "method" }
|
predicate { name == "method" }
|
||||||
@@ -66,7 +66,7 @@ object MatchingTest : PatcherTestBase() {
|
|||||||
@Test
|
@Test
|
||||||
fun `predicate matcher works correctly`() {
|
fun `predicate matcher works correctly`() {
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
execute {
|
apply {
|
||||||
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }
|
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,27 +13,7 @@ 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.StringReference
|
||||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
/**
|
@Deprecated("Use the matcher API instead.")
|
||||||
* 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 opcodes A pattern of instruction opcodes. `null` can be used as a wildcard.
|
|
||||||
* @param strings A list of the strings. Compared using [String.contains].
|
|
||||||
* @param custom A custom condition for this fingerprint.
|
|
||||||
* @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern.
|
|
||||||
*/
|
|
||||||
class Fingerprint internal constructor(
|
class Fingerprint internal constructor(
|
||||||
internal val accessFlags: Int?,
|
internal val accessFlags: Int?,
|
||||||
internal val returnType: String?,
|
internal val returnType: String?,
|
||||||
@@ -47,26 +27,10 @@ class Fingerprint internal constructor(
|
|||||||
// Backing field needed for lazy initialization.
|
// Backing field needed for lazy initialization.
|
||||||
private var _matchOrNull: Match? = null
|
private var _matchOrNull: Match? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* The match for this [Fingerprint]. Null if unmatched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
private val matchOrNull: Match?
|
private val matchOrNull: Match?
|
||||||
get() = matchOrNull()
|
get() = matchOrNull()
|
||||||
|
|
||||||
/**
|
|
||||||
* Match using [BytecodePatchContext.lookupMaps].
|
|
||||||
*
|
|
||||||
* Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match.
|
|
||||||
*
|
|
||||||
* Fingerprints can be optimized for performance:
|
|
||||||
* - Slowest: Specify [custom] or [opcodes] and nothing else.
|
|
||||||
* - Fast: Specify [accessFlags], [returnType].
|
|
||||||
* - Faster: Specify [accessFlags], [returnType] and [parameters].
|
|
||||||
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
|
|
||||||
*
|
|
||||||
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
|
|
||||||
*/
|
|
||||||
context(context: BytecodePatchContext)
|
context(context: BytecodePatchContext)
|
||||||
internal fun matchOrNull(): Match? {
|
internal fun matchOrNull(): Match? {
|
||||||
if (_matchOrNull != null) return _matchOrNull
|
if (_matchOrNull != null) return _matchOrNull
|
||||||
@@ -90,12 +54,6 @@ class Fingerprint internal constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
context(_: BytecodePatchContext)
|
||||||
fun matchOrNull(
|
fun matchOrNull(
|
||||||
classDef: ClassDef,
|
classDef: ClassDef,
|
||||||
@@ -110,25 +68,11 @@ class Fingerprint internal constructor(
|
|||||||
return null
|
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 already matched to a method, null otherwise.
|
|
||||||
*/
|
|
||||||
context(context: BytecodePatchContext)
|
context(context: BytecodePatchContext)
|
||||||
fun matchOrNull(
|
fun matchOrNull(
|
||||||
method: Method,
|
method: Method,
|
||||||
) = matchOrNull(method, context.classDefs[method.definingClass]!!)
|
) = matchOrNull(method, context.classDefs[method.definingClass]!!)
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 already matched to a method, null otherwise.
|
|
||||||
*/
|
|
||||||
context(context: BytecodePatchContext)
|
context(context: BytecodePatchContext)
|
||||||
fun matchOrNull(
|
fun matchOrNull(
|
||||||
method: Method,
|
method: Method,
|
||||||
@@ -252,171 +196,76 @@ class Fingerprint internal constructor(
|
|||||||
|
|
||||||
private val exception get() = PatchException("Failed to match the fingerprint: $this")
|
private val exception get() = PatchException("Failed to match the fingerprint: $this")
|
||||||
|
|
||||||
/**
|
|
||||||
* The match for this [Fingerprint].
|
|
||||||
*
|
|
||||||
* @throws PatchException If the [Fingerprint] has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
private val match
|
private val match
|
||||||
get() = matchOrNull ?: throw exception
|
get() = matchOrNull ?: throw exception
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
fun match(
|
fun match(
|
||||||
classDef: ClassDef,
|
classDef: ClassDef,
|
||||||
) = matchOrNull(classDef) ?: throw exception
|
) = matchOrNull(classDef) ?: throw exception
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 already matched to a method, null otherwise.
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
fun match(
|
fun match(
|
||||||
method: Method,
|
method: Method,
|
||||||
) = matchOrNull(method) ?: throw exception
|
) = matchOrNull(method) ?: throw exception
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 already matched to a method, null otherwise.
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
fun match(
|
fun match(
|
||||||
method: Method,
|
method: Method,
|
||||||
classDef: ClassDef,
|
classDef: ClassDef,
|
||||||
) = matchOrNull(method, classDef) ?: throw exception
|
) = matchOrNull(method, classDef) ?: throw exception
|
||||||
|
|
||||||
/**
|
|
||||||
* The class the matching method is a member of.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val originalClassDefOrNull
|
val originalClassDefOrNull
|
||||||
get() = matchOrNull?.originalClassDef
|
get() = matchOrNull?.originalClassDef
|
||||||
|
|
||||||
/**
|
|
||||||
* The matching method.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val originalMethodOrNull
|
val originalMethodOrNull
|
||||||
get() = matchOrNull?.originalMethod
|
get() = matchOrNull?.originalMethod
|
||||||
|
|
||||||
/**
|
|
||||||
* The mutable version of [originalClassDefOrNull].
|
|
||||||
*
|
|
||||||
* Accessing this property allocates a [ClassProxy].
|
|
||||||
* Use [originalClassDefOrNull] if mutable access is not required.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val classDefOrNull
|
val classDefOrNull
|
||||||
get() = matchOrNull?.classDef
|
get() = matchOrNull?.classDef
|
||||||
|
|
||||||
/**
|
|
||||||
* The mutable version of [originalMethodOrNull].
|
|
||||||
*
|
|
||||||
* Accessing this property allocates a [ClassProxy].
|
|
||||||
* Use [originalMethodOrNull] if mutable access is not required.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val methodOrNull
|
val methodOrNull
|
||||||
get() = matchOrNull?.method
|
get() = matchOrNull?.method
|
||||||
|
|
||||||
/**
|
|
||||||
* The match for the opcode pattern.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val patternMatchOrNull
|
val patternMatchOrNull
|
||||||
get() = matchOrNull?.patternMatch
|
get() = matchOrNull?.patternMatch
|
||||||
|
|
||||||
/**
|
|
||||||
* The matches for the strings.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val stringMatchesOrNull
|
val stringMatchesOrNull
|
||||||
get() = matchOrNull?.stringMatches
|
get() = matchOrNull?.stringMatches
|
||||||
|
|
||||||
/**
|
|
||||||
* The class the matching method is a member of.
|
|
||||||
*
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val originalClassDef
|
val originalClassDef
|
||||||
get() = match.originalClassDef
|
get() = match.originalClassDef
|
||||||
|
|
||||||
/**
|
|
||||||
* The matching method.
|
|
||||||
*
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val originalMethod
|
val originalMethod
|
||||||
get() = match.originalMethod
|
get() = match.originalMethod
|
||||||
|
|
||||||
/**
|
|
||||||
* The mutable version of [originalClassDef].
|
|
||||||
*
|
|
||||||
* Accessing this property allocates a [ClassProxy].
|
|
||||||
* Use [originalClassDef] if mutable access is not required.
|
|
||||||
*
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val classDef
|
val classDef
|
||||||
get() = match.classDef
|
get() = match.classDef
|
||||||
|
|
||||||
/**
|
|
||||||
* The mutable version of [originalMethod].
|
|
||||||
*
|
|
||||||
* Accessing this property allocates a [ClassProxy].
|
|
||||||
* Use [originalMethod] if mutable access is not required.
|
|
||||||
*
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val method
|
val method
|
||||||
get() = match.method
|
get() = match.method
|
||||||
|
|
||||||
/**
|
|
||||||
* The match for the opcode pattern.
|
|
||||||
*
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val patternMatch
|
val patternMatch
|
||||||
get() = match.patternMatch
|
get() = match.patternMatch
|
||||||
|
|
||||||
/**
|
|
||||||
* The matches for the strings.
|
|
||||||
*
|
|
||||||
* @throws PatchException If the fingerprint has not been matched.
|
|
||||||
*/
|
|
||||||
context(_: BytecodePatchContext)
|
context(_: BytecodePatchContext)
|
||||||
val stringMatches
|
val stringMatches
|
||||||
get() = match.stringMatches
|
get() = match.stringMatches
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Deprecated("Use the matcher API instead.")
|
||||||
* A match of a [Fingerprint].
|
|
||||||
*
|
|
||||||
* @param originalClassDef The class the matching method is a member of.
|
|
||||||
* @param originalMethod The matching method.
|
|
||||||
* @param patternMatch The match for the opcode pattern.
|
|
||||||
* @param stringMatches The matches for the strings.
|
|
||||||
*/
|
|
||||||
class Match internal constructor(
|
class Match internal constructor(
|
||||||
val context: BytecodePatchContext,
|
val context: BytecodePatchContext,
|
||||||
val originalClassDef: ClassDef,
|
val originalClassDef: ClassDef,
|
||||||
@@ -424,54 +273,24 @@ class Match internal constructor(
|
|||||||
val patternMatch: PatternMatch?,
|
val patternMatch: PatternMatch?,
|
||||||
val stringMatches: List<StringMatch>?,
|
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 { context.classDefs[originalClassDef.type]!! }
|
|
||||||
|
|
||||||
/**
|
val classDef by lazy {
|
||||||
* The mutable version of [originalMethod].
|
val classDef = context.classDefs[originalClassDef.type]!!
|
||||||
*
|
|
||||||
* Accessing this property allocates a new mutable instance.
|
context.classDefs.getOrReplaceMutable(classDef)
|
||||||
* Use [originalMethod] if mutable access is not required.
|
}
|
||||||
*/
|
|
||||||
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
|
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
|
||||||
|
|
||||||
/**
|
|
||||||
* A match for an opcode pattern.
|
|
||||||
* @param startIndex The index of the first opcode of the pattern in the method.
|
|
||||||
* @param endIndex The index of the last opcode of the pattern in the method.
|
|
||||||
*/
|
|
||||||
class PatternMatch internal constructor(
|
class PatternMatch internal constructor(
|
||||||
val startIndex: Int,
|
val startIndex: Int,
|
||||||
val endIndex: Int,
|
val endIndex: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* A match for a string.
|
|
||||||
*
|
|
||||||
* @param string The string that matched.
|
|
||||||
* @param index The index of the instruction in the method.
|
|
||||||
*/
|
|
||||||
class StringMatch internal constructor(val string: String, val index: Int)
|
class StringMatch internal constructor(val string: String, val index: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Deprecated("Use the matcher API instead.")
|
||||||
* A builder for [Fingerprint].
|
|
||||||
*
|
|
||||||
* @property accessFlags The exact access flags using values of [AccessFlags].
|
|
||||||
* @property returnType The return type compared using [String.startsWith].
|
|
||||||
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
|
|
||||||
* @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
|
|
||||||
* @property strings A list of the strings compared each using [String.contains].
|
|
||||||
* @property customBlock A custom condition for this fingerprint.
|
|
||||||
* @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning.
|
|
||||||
*
|
|
||||||
* @constructor Create a new [FingerprintBuilder].
|
|
||||||
*/
|
|
||||||
class FingerprintBuilder internal constructor(
|
class FingerprintBuilder internal constructor(
|
||||||
private val fuzzyPatternScanThreshold: Int = 0,
|
private val fuzzyPatternScanThreshold: Int = 0,
|
||||||
) {
|
) {
|
||||||
@@ -482,63 +301,26 @@ class FingerprintBuilder internal constructor(
|
|||||||
private var strings: List<String>? = null
|
private var strings: List<String>? = null
|
||||||
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = 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) {
|
fun accessFlags(accessFlags: Int) {
|
||||||
this.accessFlags = accessFlags
|
this.accessFlags = accessFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the access flags.
|
|
||||||
*
|
|
||||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
|
||||||
*/
|
|
||||||
fun accessFlags(vararg accessFlags: AccessFlags) {
|
fun accessFlags(vararg accessFlags: AccessFlags) {
|
||||||
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
|
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the return type.
|
|
||||||
*
|
|
||||||
* @param returnType The return type compared using [String.startsWith].
|
|
||||||
*/
|
|
||||||
fun returns(returnType: String) {
|
fun returns(returnType: String) {
|
||||||
this.returnType = returnType
|
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) {
|
fun parameters(vararg parameters: String) {
|
||||||
this.parameters = parameters.toList()
|
this.parameters = parameters.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the opcodes.
|
|
||||||
*
|
|
||||||
* @param opcodes An opcode pattern of instructions.
|
|
||||||
* Wildcard or unknown opcodes can be specified by `null`.
|
|
||||||
*/
|
|
||||||
fun opcodes(vararg opcodes: Opcode?) {
|
fun opcodes(vararg opcodes: Opcode?) {
|
||||||
this.opcodes = opcodes.toList()
|
this.opcodes = opcodes.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the opcodes.
|
|
||||||
*
|
|
||||||
* @param instructions A list of instructions or opcode names in SMALI format.
|
|
||||||
* - Wildcard or unknown opcodes can be specified by `null`.
|
|
||||||
* - Empty lines are ignored.
|
|
||||||
* - Each instruction must be on a new line.
|
|
||||||
* - The opcode name is enough, no need to specify the operands.
|
|
||||||
*
|
|
||||||
* @throws Exception If an unknown opcode is used.
|
|
||||||
*/
|
|
||||||
fun opcodes(instructions: String) {
|
fun opcodes(instructions: String) {
|
||||||
this.opcodes = instructions.trimIndent().split("\n").filter {
|
this.opcodes = instructions.trimIndent().split("\n").filter {
|
||||||
it.isNotBlank()
|
it.isNotBlank()
|
||||||
@@ -551,20 +333,10 @@ class FingerprintBuilder internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the strings.
|
|
||||||
*
|
|
||||||
* @param strings A list of strings compared each using [String.contains].
|
|
||||||
*/
|
|
||||||
fun strings(vararg strings: String) {
|
fun strings(vararg strings: String) {
|
||||||
this.strings = strings.toList()
|
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) {
|
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
|
||||||
this.customBlock = customBlock
|
this.customBlock = customBlock
|
||||||
}
|
}
|
||||||
@@ -584,14 +356,7 @@ class FingerprintBuilder internal constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Deprecated("Use the matcher API instead.")
|
||||||
* Create a [Fingerprint].
|
|
||||||
*
|
|
||||||
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
|
|
||||||
* @param block The block to build the [Fingerprint].
|
|
||||||
*
|
|
||||||
* @return The created [Fingerprint].
|
|
||||||
*/
|
|
||||||
fun fingerprint(
|
fun fingerprint(
|
||||||
fuzzyPatternScanThreshold: Int = 0,
|
fuzzyPatternScanThreshold: Int = 0,
|
||||||
block: FingerprintBuilder.() -> Unit,
|
block: FingerprintBuilder.() -> Unit,
|
||||||
|
|||||||
@@ -38,11 +38,7 @@ fun patcher(
|
|||||||
val patches = getPatches(packageName, versionName)
|
val patches = getPatches(packageName, versionName)
|
||||||
|
|
||||||
return { emit: (PatchResult) -> Unit ->
|
return { emit: (PatchResult) -> Unit ->
|
||||||
if (patches.any { patch ->
|
if (patches.any { patch -> patch.patchesResources }) resourcePatchContext.decodeResources()
|
||||||
fun Patch.check(): Boolean = type == PatchType.RESOURCE || dependencies.any { it.check() }
|
|
||||||
patch.check()
|
|
||||||
}
|
|
||||||
) resourcePatchContext.decodeResources()
|
|
||||||
|
|
||||||
// After initializing the resource context, to keep memory usage time low.
|
// After initializing the resource context, to keep memory usage time low.
|
||||||
val bytecodePatchContext = BytecodePatchContext(
|
val bytecodePatchContext = BytecodePatchContext(
|
||||||
@@ -54,74 +50,67 @@ fun patcher(
|
|||||||
|
|
||||||
bytecodePatchContext.classDefs.initializeCache()
|
bytecodePatchContext.classDefs.initializeCache()
|
||||||
|
|
||||||
logger.info("Executing patches")
|
logger.info("Applying patches")
|
||||||
|
|
||||||
patches.execute(bytecodePatchContext, resourcePatchContext, emit)
|
patches.apply(bytecodePatchContext, resourcePatchContext, emit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public for testing.
|
// Public for testing.
|
||||||
fun Set<Patch>.execute(
|
fun Set<Patch>.apply(
|
||||||
bytecodePatchContext: BytecodePatchContext,
|
bytecodePatchContext: BytecodePatchContext,
|
||||||
resourcePatchContext: ResourcePatchContext,
|
resourcePatchContext: ResourcePatchContext,
|
||||||
emit: (PatchResult) -> Unit
|
emit: (PatchResult) -> Unit
|
||||||
): PatchesResult {
|
): PatchesResult {
|
||||||
val executedPatches = LinkedHashMap<Patch, PatchResult>()
|
val appliedPatches = LinkedHashMap<Patch, PatchResult>()
|
||||||
|
|
||||||
sortedBy { it.name }.forEach { patch ->
|
sortedBy { it.name }.forEach { patch ->
|
||||||
fun Patch.execute(): PatchResult {
|
fun Patch.apply(): PatchResult {
|
||||||
val result = executedPatches[this]
|
val result = appliedPatches[this]
|
||||||
if (result != null) {
|
|
||||||
if (result.exception == null) return result
|
|
||||||
return patchResult(PatchException("The patch '$this' has failed previously"))
|
|
||||||
}
|
|
||||||
|
|
||||||
val failedDependency = dependencies.asSequence().map { it.execute() }.firstOrNull { it.exception != null }
|
return if (result == null) {
|
||||||
if (failedDependency != null) {
|
val failedDependency = dependencies.asSequence().map { it.apply() }.firstOrNull { it.exception != null }
|
||||||
return patchResult(
|
if (failedDependency != null) return patchResult(
|
||||||
PatchException(
|
"The dependant patch \"$failedDependency\" of the patch \"$this\" raised an exception:\n" +
|
||||||
"The dependant patch \"$failedDependency\" of the patch \"$this\"" +
|
failedDependency.exception!!.stackTraceToString(),
|
||||||
" raised an exception:\n${failedDependency.exception!!.stackTraceToString()}",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
val exception = runCatching {
|
val exception = runCatching { apply(bytecodePatchContext, resourcePatchContext) }
|
||||||
execute(bytecodePatchContext, resourcePatchContext)
|
.exceptionOrNull() as? Exception
|
||||||
}.exceptionOrNull() as? Exception
|
|
||||||
|
|
||||||
return patchResult(exception).also { executedPatches[this] = it }
|
patchResult(exception).also { result -> appliedPatches[this] = result }
|
||||||
|
} else if (result.exception == null) result
|
||||||
|
else patchResult("The patch '$this' has failed previously")
|
||||||
}
|
}
|
||||||
|
|
||||||
val patchResult = patch.execute()
|
val patchResult = patch.apply()
|
||||||
|
|
||||||
// If an exception occurred or the patch has no finalize block, emit the result.
|
// If an exception occurred or the patch has no finalize block, emit the result.
|
||||||
if (patchResult.exception != null || patch.finalize == null) {
|
if (patchResult.exception != null || patch.afterDependents == null) {
|
||||||
emit(patchResult)
|
emit(patchResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val succeededPatchesWithFinalizeBlock = executedPatches.values.filter {
|
val succeededPatchesWithFinalizeBlock = appliedPatches.values.filter {
|
||||||
it.exception == null && it.patch.finalize != null
|
it.exception == null && it.patch.afterDependents != null
|
||||||
}
|
}
|
||||||
|
|
||||||
succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult ->
|
succeededPatchesWithFinalizeBlock.asReversed().forEach { result ->
|
||||||
val patch = executionResult.patch
|
val patch = result.patch
|
||||||
runCatching { patch.finalize!!.invoke(bytecodePatchContext, resourcePatchContext) }
|
runCatching { patch.afterDependents!!.invoke(bytecodePatchContext, resourcePatchContext) }.fold(
|
||||||
.fold(
|
{ emit(result) },
|
||||||
{ emit(executionResult) },
|
{
|
||||||
{
|
emit(
|
||||||
emit(
|
PatchResult(
|
||||||
PatchResult(
|
patch,
|
||||||
patch,
|
PatchException(
|
||||||
PatchException(
|
"The patch \"$patch\" raised an exception:\n" + it.stackTraceToString(),
|
||||||
"The patch \"$patch\" raised an exception:\n${it.stackTraceToString()}",
|
it,
|
||||||
it,
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get())
|
return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get())
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ enum class PatchType(internal val prefix: String) {
|
|||||||
RESOURCE("Resource")
|
RESOURCE("Resource")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val Patch.patchesResources: Boolean get() = type == PatchType.RESOURCE || dependencies.any { it.patchesResources }
|
||||||
|
|
||||||
open class Patch internal constructor(
|
open class Patch internal constructor(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
@@ -27,10 +29,10 @@ open class Patch internal constructor(
|
|||||||
val dependencies: Set<Patch>,
|
val dependencies: Set<Patch>,
|
||||||
val compatiblePackages: Set<Package>?,
|
val compatiblePackages: Set<Package>?,
|
||||||
options: Set<Option<*>>,
|
options: Set<Option<*>>,
|
||||||
internal val execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit,
|
internal val apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit,
|
||||||
// Must be nullable, so that Patcher.invoke can check,
|
// Must be nullable, so that Patcher.invoke can check,
|
||||||
// if a patch has "finalize" in order to not emit it twice.
|
// if a patch has an "afterDependents" in order to not emit it twice.
|
||||||
internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?,
|
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?,
|
||||||
internal val type: PatchType,
|
internal val type: PatchType,
|
||||||
) {
|
) {
|
||||||
val options = Options(options)
|
val options = Options(options)
|
||||||
@@ -46,18 +48,18 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||||||
private val dependencies = mutableSetOf<Patch>()
|
private val dependencies = mutableSetOf<Patch>()
|
||||||
private val options = mutableSetOf<Option<*>>()
|
private val options = mutableSetOf<Option<*>>()
|
||||||
|
|
||||||
internal var execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { }
|
internal var apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { }
|
||||||
internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null
|
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null
|
||||||
|
|
||||||
context(_: BytecodePatchContext, _: ResourcePatchContext)
|
context(_: BytecodePatchContext, _: ResourcePatchContext)
|
||||||
private val patchContext get() = getPatchContext()
|
private val patchContext get() = getPatchContext()
|
||||||
|
|
||||||
fun execute(block: C.() -> Unit) {
|
fun apply(block: C.() -> Unit) {
|
||||||
execute = { block(patchContext) }
|
apply = { block(patchContext) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finalize(block: C.() -> Unit) {
|
fun afterDependents(block: C.() -> Unit) {
|
||||||
finalize = { block(patchContext) }
|
afterDependents = { block(patchContext) }
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun <T> Option<T>.invoke() = apply {
|
operator fun <T> Option<T>.invoke() = apply {
|
||||||
@@ -90,8 +92,8 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||||||
dependencies,
|
dependencies,
|
||||||
compatiblePackages,
|
compatiblePackages,
|
||||||
options,
|
options,
|
||||||
execute,
|
apply,
|
||||||
finalize,
|
afterDependents,
|
||||||
type,
|
type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -101,7 +103,7 @@ class BytecodePatchBuilder private constructor(
|
|||||||
) : PatchBuilder<BytecodePatchContext>(
|
) : PatchBuilder<BytecodePatchContext>(
|
||||||
PatchType.BYTECODE,
|
PatchType.BYTECODE,
|
||||||
{
|
{
|
||||||
// Extend the context with the extension, before returning it to the patch for execution.
|
// Extend the context with the extension, before returning it to the patch before applying it.
|
||||||
contextOf<BytecodePatchContext>().apply {
|
contextOf<BytecodePatchContext>().apply {
|
||||||
if (extensionInputStream != null) extendWith(extensionInputStream)
|
if (extensionInputStream != null) extendWith(extensionInputStream)
|
||||||
}
|
}
|
||||||
@@ -109,7 +111,7 @@ class BytecodePatchBuilder private constructor(
|
|||||||
) {
|
) {
|
||||||
internal constructor() : this(null)
|
internal constructor() : this(null)
|
||||||
|
|
||||||
fun extendWith(extension: String) = apply {
|
fun extendWith(extension: String) {
|
||||||
// Should be the classloader which loaded the patch class.
|
// Should be the classloader which loaded the patch class.
|
||||||
val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!!
|
val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!!
|
||||||
|
|
||||||
@@ -198,9 +200,9 @@ class PatchException(errorMessage: String?, cause: Throwable?) : Exception(error
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A result of executing a [Patch].
|
* A result of applying a [Patch].
|
||||||
*
|
*
|
||||||
* @param patch The [Patch] that was executed.
|
* @param patch The [Patch] that ran.
|
||||||
* @param exception The [PatchException] thrown, if any.
|
* @param exception The [PatchException] thrown, if any.
|
||||||
*/
|
*/
|
||||||
class PatchResult internal constructor(val patch: Patch, val exception: PatchException? = null)
|
class PatchResult internal constructor(val patch: Patch, val exception: PatchException? = null)
|
||||||
@@ -212,6 +214,14 @@ class PatchResult internal constructor(val patch: Patch, val exception: PatchExc
|
|||||||
* @return The created [PatchResult].
|
* @return The created [PatchResult].
|
||||||
*/
|
*/
|
||||||
internal fun Patch.patchResult(exception: Exception? = null) = PatchResult(this, exception?.toPatchException())
|
internal fun Patch.patchResult(exception: Exception? = null) = PatchResult(this, exception?.toPatchException())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [PatchResult] for this [Patch] with the given error message.
|
||||||
|
*
|
||||||
|
* @param errorMessage The error message.
|
||||||
|
* @return The created [PatchResult].
|
||||||
|
*/
|
||||||
|
internal fun Patch.patchResult(errorMessage: String) = PatchResult(this, PatchException(errorMessage))
|
||||||
private fun Exception.toPatchException() = this as? PatchException ?: PatchException(this)
|
private fun Exception.toPatchException() = this as? PatchException ?: PatchException(this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method {
|
|||||||
private var accessFlags = method.accessFlags
|
private var accessFlags = method.accessFlags
|
||||||
private var returnType = method.returnType
|
private var returnType = method.returnType
|
||||||
|
|
||||||
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count)
|
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count).
|
||||||
private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } }
|
private var implementation = method.implementation?.let(::MutableMethodImplementation)
|
||||||
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
|
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
|
||||||
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
|
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
|
||||||
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
|
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
|
||||||
@@ -35,6 +35,10 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method {
|
|||||||
this.returnType = returnType
|
this.returnType = returnType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setImplementation(implementation: MutableMethodImplementation?) {
|
||||||
|
this.implementation = implementation
|
||||||
|
}
|
||||||
|
|
||||||
override fun getDefiningClass() = definingClass
|
override fun getDefiningClass() = definingClass
|
||||||
|
|
||||||
override fun getName() = name
|
override fun getName() = name
|
||||||
@@ -51,7 +55,7 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method {
|
|||||||
|
|
||||||
override fun getParameters() = _parameters
|
override fun getParameters() = _parameters
|
||||||
|
|
||||||
override fun getImplementation() = _implementation
|
override fun getImplementation() = implementation
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun Method.toMutable() = MutableMethod(this)
|
fun Method.toMutable() = MutableMethod(this)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class FingerprintTest : PatcherTestBase() {
|
||||||
|
@BeforeAll
|
||||||
|
fun setup() = setupMock()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `matches fingerprints correctly`() {
|
||||||
|
with(bytecodePatchContext) {
|
||||||
|
assertNotNull(
|
||||||
|
fingerprint { returns("V") }.originalMethodOrNull,
|
||||||
|
"Fingerprints should match correctly."
|
||||||
|
)
|
||||||
|
assertNull(
|
||||||
|
fingerprint { returns("doesnt exist") }.originalMethodOrNull,
|
||||||
|
"Fingerprints should match correctly."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,154 +5,87 @@ import app.revanced.patcher.patch.PatchException
|
|||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import org.junit.jupiter.api.BeforeAll
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.assertAll
|
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
internal class PatcherTest : PatcherTestBase() {
|
internal class PatcherTest : PatcherTestBase() {
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
fun setUp() = setUpMock()
|
fun setup() = setupMock()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `executes patches in correct order`() {
|
fun `applies patches in correct order`() {
|
||||||
val executed = mutableListOf<String>()
|
val applied = mutableListOf<String>()
|
||||||
|
|
||||||
val patches = setOf(
|
infix fun Patch.resultsIn(equals: List<String>) = this to equals
|
||||||
bytecodePatch { execute { executed += "1" } },
|
infix fun Pair<Patch, List<String>>.because(reason: String) {
|
||||||
bytecodePatch {
|
runCatching { setOf(first)() }
|
||||||
dependsOn(
|
|
||||||
bytecodePatch {
|
|
||||||
execute { executed += "2" }
|
|
||||||
finalize { executed += "-2" }
|
|
||||||
},
|
|
||||||
bytecodePatch { execute { executed += "3" } },
|
|
||||||
)
|
|
||||||
|
|
||||||
execute { executed += "4" }
|
assertEquals(second, applied, reason)
|
||||||
finalize { executed += "-1" }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert(executed.isEmpty())
|
applied.clear()
|
||||||
|
|
||||||
patches()
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
listOf("1", "2", "3", "4", "-1", "-2"),
|
|
||||||
executed,
|
|
||||||
"Expected patches to be executed in correct order.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `handles execution of patches correctly when exceptions occur`() {
|
|
||||||
val executed = mutableListOf<String>()
|
|
||||||
|
|
||||||
infix fun Patch.resultsIn(equals: List<String>) {
|
|
||||||
val patches = setOf(this)
|
|
||||||
|
|
||||||
try {
|
|
||||||
patches()
|
|
||||||
} catch (_: PatchException) {
|
|
||||||
// Swallow expected exceptions for testing purposes.
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(equals, executed, "Expected patches to be executed in correct order.")
|
|
||||||
|
|
||||||
executed.clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No patches execute successfully,
|
|
||||||
// because the dependency patch throws an exception inside the execute block.
|
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
dependsOn(
|
dependsOn(
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
execute { throw PatchException("1") }
|
apply { applied += "1" }
|
||||||
finalize { executed += "-2" }
|
afterDependents { applied += "-2" }
|
||||||
},
|
},
|
||||||
|
bytecodePatch { apply { applied += "2" } },
|
||||||
)
|
)
|
||||||
|
apply { applied += "3" }
|
||||||
|
afterDependents { applied += "-1" }
|
||||||
|
} resultsIn listOf("1", "2", "3", "-1", "-2") because
|
||||||
|
"Patches should apply in post-order and afterDependents in pre-order."
|
||||||
|
|
||||||
execute { executed += "2" }
|
|
||||||
finalize { executed += "-1" }
|
|
||||||
} resultsIn emptyList()
|
|
||||||
|
|
||||||
// The dependency patch is executed successfully,
|
|
||||||
// because only the dependant patch throws an exception inside the execute block.
|
|
||||||
// Patches that depend on a failed patch should not be executed,
|
|
||||||
// but patches that are dependant by a failed patch should be finalized.
|
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
dependsOn(
|
dependsOn(
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
execute { executed += "1" }
|
apply { throw PatchException("1") }
|
||||||
finalize { executed += "-2" }
|
afterDependents { applied += "-2" }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
apply { applied += "2" }
|
||||||
|
afterDependents { applied += "-1" }
|
||||||
|
} resultsIn emptyList() because
|
||||||
|
"Patches that depend on a patched that failed to apply should not be applied."
|
||||||
|
|
||||||
execute { throw PatchException("2") }
|
|
||||||
finalize { executed += "-1" }
|
|
||||||
} resultsIn listOf("1", "-2")
|
|
||||||
|
|
||||||
// Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch,
|
|
||||||
// the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block.
|
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
dependsOn(
|
dependsOn(
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
execute { executed += "1" }
|
apply { applied += "1" }
|
||||||
finalize { throw PatchException("-2") }
|
afterDependents { applied += "-2" }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
apply { throw PatchException("2") }
|
||||||
|
afterDependents { applied += "-1" }
|
||||||
|
} resultsIn listOf("1", "-2") because
|
||||||
|
"afterDependents of a patch should not be called if it failed to apply."
|
||||||
|
|
||||||
execute { executed += "2" }
|
|
||||||
finalize { executed += "-1" }
|
|
||||||
} resultsIn listOf("1", "2", "-1")
|
|
||||||
|
|
||||||
// The dependency patch is executed successfully,
|
|
||||||
// because the dependant patch raises an exception in the finalize block.
|
|
||||||
// Patches that depend on a failed patch should not be executed,
|
|
||||||
// but patches that are depended on by a failed patch should be executed.
|
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
dependsOn(
|
dependsOn(
|
||||||
bytecodePatch {
|
bytecodePatch {
|
||||||
execute { executed += "1" }
|
apply { applied += "1" }
|
||||||
finalize { executed += "-2" }
|
afterDependents { applied += "-2" }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
apply { applied += "2" }
|
||||||
execute { executed += "2" }
|
afterDependents { throw PatchException("-1") }
|
||||||
finalize { throw PatchException("-1") }
|
} resultsIn listOf("1", "2", "-2") because
|
||||||
} resultsIn listOf("1", "2", "-2")
|
"afterDependents of a patch should be called " +
|
||||||
|
"regardless of dependant patches failing."
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `throws if unmatched fingerprint match is used`() {
|
fun `throws if unmatched fingerprint match is used`() {
|
||||||
with(bytecodePatchContext) {
|
with(bytecodePatchContext) {
|
||||||
val fingerprint = fingerprint {
|
val fingerprint = fingerprint { strings("doesnt exist") }
|
||||||
strings("doesnt exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows<PatchException>("Expected an exception because the fingerprint can't match.") {
|
assertThrows<PatchException>("Expected an exception because the fingerprint can't match.") {
|
||||||
fingerprint.patternMatch
|
fingerprint.patternMatch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `matches fingerprint`() {
|
|
||||||
val fingerprint = fingerprint { returns("V") }
|
|
||||||
val fingerprint2 = fingerprint { returns("V") }
|
|
||||||
val fingerprint3 = fingerprint { returns("V") }
|
|
||||||
|
|
||||||
with(bytecodePatchContext) {
|
|
||||||
assertAll(
|
|
||||||
"Expected fingerprints to match.",
|
|
||||||
{ assertNotNull(fingerprint.matchOrNull(this.classDefs.first().methods.first())) },
|
|
||||||
{ assertNotNull(fingerprint2.matchOrNull(this.classDefs.first())) },
|
|
||||||
{ assertNotNull(fingerprint3.originalClassDefOrNull) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package app.revanced.patcher
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod
|
|
||||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
|
||||||
import app.revanced.patcher.extensions.*
|
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||||
@@ -10,28 +7,30 @@ import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
|||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
|
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||||
|
import com.android.tools.smali.dexlib2.mutable.MutableMethod
|
||||||
|
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
private object InstructionExtensionsTest {
|
internal class MethodExtensionsTest {
|
||||||
private lateinit var testMethod: MutableMethod
|
private val testInstructions = (0..9).map { i -> TestInstruction(i) }
|
||||||
private lateinit var testMethodImplementation: MutableMethodImplementation
|
private var method = ImmutableMethod(
|
||||||
|
"TestClass;",
|
||||||
|
"testMethod",
|
||||||
|
null,
|
||||||
|
"V",
|
||||||
|
AccessFlags.PUBLIC.value,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
MutableMethodImplementation(16)
|
||||||
|
).toMutable()
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun createTestMethod() =
|
fun setup() {
|
||||||
ImmutableMethod(
|
method.instructions.clear()
|
||||||
"TestClass;",
|
method.addInstructions(testInstructions)
|
||||||
"testMethod",
|
}
|
||||||
null,
|
|
||||||
"V",
|
|
||||||
AccessFlags.PUBLIC.value,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
|
|
||||||
repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
|
|
||||||
},
|
|
||||||
).let { testMethod = it.toMutable() }
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun addInstructionsToImplementationIndexed() =
|
fun addInstructionsToImplementationIndexed() =
|
||||||
@@ -219,11 +218,11 @@ private object InstructionExtensionsTest {
|
|||||||
// region Helper methods
|
// region Helper methods
|
||||||
|
|
||||||
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
|
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
|
||||||
testMethodImplementation.apply(block)
|
method.implementation!!.apply(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyToMethod(block: MutableMethod.() -> Unit) {
|
private fun applyToMethod(block: MutableMethod.() -> Unit) {
|
||||||
testMethod.apply(block)
|
method.apply(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableMethodImplementation.assertRegisterIs(
|
private fun MutableMethodImplementation.assertRegisterIs(
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
package app.revanced.patcher.patch.options
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.patch.*
|
import app.revanced.patcher.patch.PatchBuilder.invoke
|
||||||
import org.junit.jupiter.api.assertDoesNotThrow
|
import org.junit.jupiter.api.assertDoesNotThrow
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
import kotlin.test.*
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
internal object OptionsTest {
|
internal object OptionsTest {
|
||||||
private val externalOption = stringOption("external", "default")
|
private val externalOption = stringOption("external", "default")
|
||||||
@@ -141,4 +144,4 @@ internal object OptionsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
|
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
|
||||||
}
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
@file:Suppress("unused")
|
|
||||||
|
|
||||||
package app.revanced.patcher.patch
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.reflect.jvm.javaField
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
internal object PatchLoaderTest {
|
|
||||||
@Test
|
|
||||||
fun `loads patches correctly`() {
|
|
||||||
val patchesClass = ::Public.javaField!!.declaringClass.name
|
|
||||||
val classLoader = ::Public.javaClass.classLoader
|
|
||||||
|
|
||||||
val patches = getPatches(listOf(patchesClass), classLoader)
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
2,
|
|
||||||
patches.size,
|
|
||||||
"Expected 2 patches to be loaded, " +
|
|
||||||
"because there's only two named patches declared as public static fields " +
|
|
||||||
"or returned by public static and non-parametrized methods.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed.
|
|
||||||
|
|
||||||
val Public by creatingBytecodePatch {} // Loaded, because it's named.
|
|
||||||
|
|
||||||
private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private.
|
|
||||||
|
|
||||||
private val Private by creatingBytecodePatch {} // Not loaded, because it's private.
|
|
||||||
|
|
||||||
fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed.
|
|
||||||
|
|
||||||
fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named.
|
|
||||||
|
|
||||||
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) =
|
|
||||||
publicNamedPatchFunction() // Not loaded, because it's parameterized.
|
|
||||||
|
|
||||||
private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private.
|
|
||||||
|
|
||||||
private fun privateNamedPatchFunction() = Private // Not loaded, because it's private.
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
import kotlin.reflect.jvm.javaField
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ internal object PatchTest {
|
|||||||
val print by stringOption("print")
|
val print by stringOption("print")
|
||||||
val custom = option<String>("custom")()
|
val custom = option<String>("custom")()
|
||||||
|
|
||||||
execute {
|
this.apply {
|
||||||
println(print)
|
println(print)
|
||||||
println(custom.value)
|
println(custom.value)
|
||||||
}
|
}
|
||||||
@@ -46,4 +47,39 @@ internal object PatchTest {
|
|||||||
|
|
||||||
assertEquals(2, patch.options.size)
|
assertEquals(2, patch.options.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loads patches correctly`() {
|
||||||
|
val patchesClass = ::Public.javaField!!.declaringClass.name
|
||||||
|
val classLoader = ::Public.javaClass.classLoader
|
||||||
|
|
||||||
|
val patches = getPatches(listOf(patchesClass), classLoader)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
2,
|
||||||
|
patches.size,
|
||||||
|
"Expected 2 patches to be loaded, " +
|
||||||
|
"because there's only two named patches declared as public static fields " +
|
||||||
|
"or returned by public static and non-parametrized methods.",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed.
|
||||||
|
|
||||||
|
val Public by creatingBytecodePatch {} // Loaded, because it's named.
|
||||||
|
|
||||||
|
private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private.
|
||||||
|
|
||||||
|
private val Private by creatingBytecodePatch {} // Not loaded, because it's private.
|
||||||
|
|
||||||
|
fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed.
|
||||||
|
|
||||||
|
fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named.
|
||||||
|
|
||||||
|
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) =
|
||||||
|
publicNamedPatchFunction() // Not loaded, because it's parameterized.
|
||||||
|
|
||||||
|
private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private.
|
||||||
|
|
||||||
|
private fun privateNamedPatchFunction() = Private // Not loaded, because it's private.
|
||||||
|
|||||||
@@ -1,43 +1,39 @@
|
|||||||
package app.revanced.patcher.util
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
|
||||||
import app.revanced.patcher.extensions.*
|
import app.revanced.patcher.extensions.*
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
|
||||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
|
|
||||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
|
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
|
||||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||||
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
|
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
||||||
import java.util.*
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
internal object SmaliTest {
|
internal class SmaliTest {
|
||||||
@Test
|
val method = ImmutableMethod(
|
||||||
fun `outputs valid instruction`() {
|
"Ldummy;",
|
||||||
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
|
"name",
|
||||||
val have = "const-string v0, \"Test\"".toInstructions().first()
|
emptyList(), // parameters
|
||||||
|
"V",
|
||||||
|
AccessFlags.PUBLIC.value,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
MutableMethodImplementation(1),
|
||||||
|
).toMutable()
|
||||||
|
|
||||||
assertInstructionsEqual(want, have)
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
method.instructions.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `supports branching with own branches`() {
|
fun `own branches work`() {
|
||||||
val method = createMethod()
|
|
||||||
val instructionCount = 8
|
|
||||||
val instructionIndex = instructionCount - 2
|
|
||||||
val targetIndex = instructionIndex - 1
|
|
||||||
|
|
||||||
method.addInstructions(
|
|
||||||
arrayOfNulls<String>(instructionCount).also {
|
|
||||||
Arrays.fill(it, "const/4 v0, 0x0")
|
|
||||||
}.joinToString("\n"),
|
|
||||||
)
|
|
||||||
method.addInstructionsWithLabels(
|
method.addInstructionsWithLabels(
|
||||||
targetIndex,
|
0,
|
||||||
"""
|
"""
|
||||||
:test
|
:test
|
||||||
const/4 v0, 0x1
|
const/4 v0, 0x1
|
||||||
@@ -45,14 +41,13 @@ internal object SmaliTest {
|
|||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
|
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(0).target.location.index
|
||||||
|
|
||||||
assertEquals(targetIndex, instruction.target.location.index)
|
assertEquals(0, targetLocationIndex, "Label should point to index 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `supports branching to outside branches`() {
|
fun `external branches work`() {
|
||||||
val method = createMethod()
|
|
||||||
val instructionIndex = 3
|
val instructionIndex = 3
|
||||||
val labelIndex = 1
|
val labelIndex = 1
|
||||||
|
|
||||||
@@ -63,10 +58,8 @@ internal object SmaliTest {
|
|||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(labelIndex, method.newLabel(labelIndex).location.index)
|
|
||||||
|
|
||||||
method.addInstructionsWithLabels(
|
method.addInstructionsWithLabels(
|
||||||
method.implementation!!.instructions.size,
|
method.instructions.size,
|
||||||
"""
|
"""
|
||||||
const/4 v0, 0x1
|
const/4 v0, 0x1
|
||||||
if-eqz v0, :test
|
if-eqz v0, :test
|
||||||
@@ -76,29 +69,8 @@ internal object SmaliTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
|
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
|
||||||
assertTrue(instruction.target.isPlaced, "Label was not placed")
|
|
||||||
|
assertTrue(instruction.target.isPlaced, "Label should be placed")
|
||||||
assertEquals(labelIndex, instruction.target.location.index)
|
assertEquals(labelIndex, instruction.target.location.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMethod(
|
|
||||||
name: String = "dummy",
|
|
||||||
returnType: String = "V",
|
|
||||||
accessFlags: Int = AccessFlags.STATIC.value,
|
|
||||||
registerCount: Int = 1,
|
|
||||||
) = ImmutableMethod(
|
|
||||||
"Ldummy;",
|
|
||||||
name,
|
|
||||||
emptyList(), // parameters
|
|
||||||
returnType,
|
|
||||||
accessFlags,
|
|
||||||
emptySet(),
|
|
||||||
emptySet(),
|
|
||||||
MutableMethodImplementation(registerCount),
|
|
||||||
).toMutable()
|
|
||||||
|
|
||||||
private fun assertInstructionsEqual(want: BuilderInstruction, have: BuilderInstruction) {
|
|
||||||
assertEquals(want.opcode, have.opcode)
|
|
||||||
assertEquals(want.format, have.format)
|
|
||||||
assertEquals(want.codeUnits, have.codeUnits)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ abstract class PatcherTestBase {
|
|||||||
protected lateinit var bytecodePatchContext: BytecodePatchContext
|
protected lateinit var bytecodePatchContext: BytecodePatchContext
|
||||||
protected lateinit var resourcePatchContext: ResourcePatchContext
|
protected lateinit var resourcePatchContext: ResourcePatchContext
|
||||||
|
|
||||||
protected fun setUpMock(
|
protected fun setupMock(
|
||||||
method: ImmutableMethod = ImmutableMethod(
|
method: ImmutableMethod = ImmutableMethod(
|
||||||
"class",
|
"class",
|
||||||
"method",
|
"method",
|
||||||
@@ -89,7 +89,7 @@ abstract class PatcherTestBase {
|
|||||||
|
|
||||||
protected operator fun Set<Patch>.invoke() {
|
protected operator fun Set<Patch>.invoke() {
|
||||||
runCatching {
|
runCatching {
|
||||||
execute(
|
apply(
|
||||||
bytecodePatchContext,
|
bytecodePatchContext,
|
||||||
resourcePatchContext
|
resourcePatchContext
|
||||||
) { }
|
) { }
|
||||||
|
|||||||
Reference in New Issue
Block a user