diff --git a/matching/src/commonTest/kotlin/app/revanced/patcher/MatchingTest.kt b/matching/src/commonTest/kotlin/app/revanced/patcher/MatchingTest.kt index bc0e9ad..872d6f1 100644 --- a/matching/src/commonTest/kotlin/app/revanced/patcher/MatchingTest.kt +++ b/matching/src/commonTest/kotlin/app/revanced/patcher/MatchingTest.kt @@ -15,7 +15,7 @@ import kotlin.test.assertNotNull @TestInstance(TestInstance.Lifecycle.PER_CLASS) object MatchingTest : PatcherTestBase() { @BeforeAll - fun setUp() = setUpMock() + fun setup() = setupMock() @Test fun `matches via builder api`() { @@ -37,7 +37,7 @@ object MatchingTest : PatcherTestBase() { } bytecodePatch { - execute { + apply { assertNotNull(firstMethodBuilder().methodOrNull) { "Expected to find a method" } Assertions.assertNull(firstMethodBuilder(fail = true).methodOrNull) { "Expected to not find a method" } } @@ -47,7 +47,7 @@ object MatchingTest : PatcherTestBase() { @Test fun `matches via declarative api`() { bytecodePatch { - execute { + apply { val method = firstMethodByDeclarativePredicateOrNull { anyOf { predicate { name == "method" } @@ -66,7 +66,7 @@ object MatchingTest : PatcherTestBase() { @Test fun `predicate matcher works correctly`() { bytecodePatch { - execute { + apply { assertDoesNotThrow("Should find method") { firstMethod { name == "method" } } } } diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt index 59afabb..ded0942 100644 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt @@ -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.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 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. - */ +@Deprecated("Use the matcher API instead.") class Fingerprint internal constructor( internal val accessFlags: Int?, internal val returnType: String?, @@ -47,26 +27,10 @@ class Fingerprint internal constructor( // Backing field needed for lazy initialization. private var _matchOrNull: Match? = null - /** - * The match for this [Fingerprint]. Null if unmatched. - */ context(_: BytecodePatchContext) private val matchOrNull: Match? 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) internal fun matchOrNull(): Match? { if (_matchOrNull != null) return _matchOrNull @@ -90,12 +54,6 @@ class Fingerprint internal constructor( 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) fun matchOrNull( classDef: ClassDef, @@ -110,25 +68,11 @@ class Fingerprint internal constructor( 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) fun matchOrNull( method: Method, ) = 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) fun matchOrNull( method: Method, @@ -252,171 +196,76 @@ class Fingerprint internal constructor( 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) private val match 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) fun match( classDef: ClassDef, ) = 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) fun match( method: Method, ) = 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) fun match( method: Method, classDef: ClassDef, ) = matchOrNull(method, classDef) ?: throw exception - /** - * The class the matching method is a member of. - */ context(_: BytecodePatchContext) val originalClassDefOrNull get() = matchOrNull?.originalClassDef - /** - * The matching method. - */ context(_: BytecodePatchContext) val originalMethodOrNull get() = matchOrNull?.originalMethod - /** - * The mutable version of [originalClassDefOrNull]. - * - * Accessing this property allocates a [ClassProxy]. - * 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 [ClassProxy]. - * Use [originalMethodOrNull] if mutable access is not required. - */ context(_: BytecodePatchContext) val methodOrNull get() = matchOrNull?.method - /** - * The match for the opcode pattern. - */ context(_: BytecodePatchContext) val patternMatchOrNull get() = matchOrNull?.patternMatch - /** - * The matches for the strings. - */ context(_: BytecodePatchContext) val stringMatchesOrNull get() = matchOrNull?.stringMatches - /** - * 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 [ClassProxy]. - * 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 [ClassProxy]. - * 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) val patternMatch get() = match.patternMatch - /** - * The matches for the strings. - * - * @throws PatchException If the fingerprint has not been matched. - */ context(_: BytecodePatchContext) 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 patternMatch The match for the opcode pattern. - * @param stringMatches The matches for the strings. - */ +@Deprecated("Use the matcher API instead.") class Match internal constructor( val context: BytecodePatchContext, val originalClassDef: ClassDef, @@ -424,54 +273,24 @@ class Match internal constructor( val patternMatch: PatternMatch?, val stringMatches: List?, ) { - /** - * 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]!! } - /** - * The mutable version of [originalMethod]. - * - * Accessing this property allocates a new mutable instance. - * Use [originalMethod] if mutable access is not required. - */ + val classDef by lazy { + val classDef = context.classDefs[originalClassDef.type]!! + + context.classDefs.getOrReplaceMutable(classDef) + } + 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( val startIndex: Int, val endIndex: Int, ) - /** - * A match for a string. - * - * @param string The string that matched. - * @param index The index of the instruction in the method. - */ class StringMatch internal constructor(val string: String, val index: Int) } -/** - * A builder for [Fingerprint]. - * - * @property accessFlags The exact access flags using values of [AccessFlags]. - * @property returnType The return type compared using [String.startsWith]. - * @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType]. - * @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`. - * @property strings A list of the strings compared each using [String.contains]. - * @property customBlock A custom condition for this fingerprint. - * @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. - * - * @constructor Create a new [FingerprintBuilder]. - */ +@Deprecated("Use the matcher API instead.") class FingerprintBuilder internal constructor( private val fuzzyPatternScanThreshold: Int = 0, ) { @@ -482,63 +301,26 @@ class FingerprintBuilder internal constructor( private var strings: List? = null private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null - /** - * Set the access flags. - * - * @param accessFlags The exact access flags using values of [AccessFlags]. - */ fun accessFlags(accessFlags: Int) { this.accessFlags = accessFlags } - /** - * Set the access flags. - * - * @param accessFlags The exact access flags using values of [AccessFlags]. - */ fun accessFlags(vararg accessFlags: AccessFlags) { this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value } } - /** - * Set the return type. - * - * @param returnType The return type compared using [String.startsWith]. - */ fun returns(returnType: String) { this.returnType = returnType } - /** - * Set the parameters. - * - * @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType]. - */ fun parameters(vararg parameters: String) { this.parameters = parameters.toList() } - /** - * Set the opcodes. - * - * @param opcodes An opcode pattern of instructions. - * Wildcard or unknown opcodes can be specified by `null`. - */ fun opcodes(vararg opcodes: Opcode?) { this.opcodes = opcodes.toList() } - /** - * Set the opcodes. - * - * @param instructions A list of instructions or opcode names in SMALI format. - * - Wildcard or unknown opcodes can be specified by `null`. - * - Empty lines are ignored. - * - Each instruction must be on a new line. - * - The opcode name is enough, no need to specify the operands. - * - * @throws Exception If an unknown opcode is used. - */ fun opcodes(instructions: String) { this.opcodes = instructions.trimIndent().split("\n").filter { it.isNotBlank() @@ -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) { 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 } @@ -584,14 +356,7 @@ class FingerprintBuilder internal constructor( } } -/** - * 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]. - */ +@Deprecated("Use the matcher API instead.") fun fingerprint( fuzzyPatternScanThreshold: Int = 0, block: FingerprintBuilder.() -> Unit, diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt index 18f3914..eee24ae 100644 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt @@ -38,11 +38,7 @@ fun patcher( val patches = getPatches(packageName, versionName) return { emit: (PatchResult) -> Unit -> - if (patches.any { patch -> - fun Patch.check(): Boolean = type == PatchType.RESOURCE || dependencies.any { it.check() } - patch.check() - } - ) resourcePatchContext.decodeResources() + if (patches.any { patch -> patch.patchesResources }) resourcePatchContext.decodeResources() // After initializing the resource context, to keep memory usage time low. val bytecodePatchContext = BytecodePatchContext( @@ -54,74 +50,67 @@ fun patcher( bytecodePatchContext.classDefs.initializeCache() - logger.info("Executing patches") + logger.info("Applying patches") - patches.execute(bytecodePatchContext, resourcePatchContext, emit) + patches.apply(bytecodePatchContext, resourcePatchContext, emit) } } // Public for testing. -fun Set.execute( +fun Set.apply( bytecodePatchContext: BytecodePatchContext, resourcePatchContext: ResourcePatchContext, emit: (PatchResult) -> Unit ): PatchesResult { - val executedPatches = LinkedHashMap() + val appliedPatches = LinkedHashMap() sortedBy { it.name }.forEach { patch -> - fun Patch.execute(): PatchResult { - val result = executedPatches[this] - if (result != null) { - if (result.exception == null) return result - return patchResult(PatchException("The patch '$this' has failed previously")) - } + fun Patch.apply(): PatchResult { + val result = appliedPatches[this] - val failedDependency = dependencies.asSequence().map { it.execute() }.firstOrNull { it.exception != null } - if (failedDependency != null) { - return patchResult( - PatchException( - "The dependant patch \"$failedDependency\" of the patch \"$this\"" + - " raised an exception:\n${failedDependency.exception!!.stackTraceToString()}", - ), + return if (result == null) { + val failedDependency = dependencies.asSequence().map { it.apply() }.firstOrNull { it.exception != null } + if (failedDependency != null) return patchResult( + "The dependant patch \"$failedDependency\" of the patch \"$this\" raised an exception:\n" + + failedDependency.exception!!.stackTraceToString(), ) - } - val exception = runCatching { - execute(bytecodePatchContext, resourcePatchContext) - }.exceptionOrNull() as? Exception + val exception = runCatching { apply(bytecodePatchContext, resourcePatchContext) } + .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 (patchResult.exception != null || patch.finalize == null) { + if (patchResult.exception != null || patch.afterDependents == null) { emit(patchResult) } } - val succeededPatchesWithFinalizeBlock = executedPatches.values.filter { - it.exception == null && it.patch.finalize != null + val succeededPatchesWithFinalizeBlock = appliedPatches.values.filter { + it.exception == null && it.patch.afterDependents != null } - succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult -> - val patch = executionResult.patch - runCatching { patch.finalize!!.invoke(bytecodePatchContext, resourcePatchContext) } - .fold( - { emit(executionResult) }, - { - emit( - PatchResult( - patch, - PatchException( - "The patch \"$patch\" raised an exception:\n${it.stackTraceToString()}", - it, - ), - ) + succeededPatchesWithFinalizeBlock.asReversed().forEach { result -> + val patch = result.patch + runCatching { patch.afterDependents!!.invoke(bytecodePatchContext, resourcePatchContext) }.fold( + { emit(result) }, + { + emit( + PatchResult( + patch, + PatchException( + "The patch \"$patch\" raised an exception:\n" + it.stackTraceToString(), + it, + ), ) - } - ) + ) + } + ) } return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get()) diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt index 4e40910..f5a87a0 100644 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/patch/Patch.kt @@ -20,6 +20,8 @@ enum class PatchType(internal val prefix: String) { RESOURCE("Resource") } +internal val Patch.patchesResources: Boolean get() = type == PatchType.RESOURCE || dependencies.any { it.patchesResources } + open class Patch internal constructor( val name: String?, val description: String?, @@ -27,10 +29,10 @@ open class Patch internal constructor( val dependencies: Set, val compatiblePackages: Set?, options: Set>, - internal val execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit, + internal val apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit, // Must be nullable, so that Patcher.invoke can check, - // if a patch has "finalize" in order to not emit it twice. - internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?, + // if a patch has an "afterDependents" in order to not emit it twice. + internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?, internal val type: PatchType, ) { val options = Options(options) @@ -46,18 +48,18 @@ sealed class PatchBuilder>( private val dependencies = mutableSetOf() private val options = mutableSetOf>() - internal var execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { } - internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null + internal var apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { } + internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null context(_: BytecodePatchContext, _: ResourcePatchContext) private val patchContext get() = getPatchContext() - fun execute(block: C.() -> Unit) { - execute = { block(patchContext) } + fun apply(block: C.() -> Unit) { + apply = { block(patchContext) } } - fun finalize(block: C.() -> Unit) { - finalize = { block(patchContext) } + fun afterDependents(block: C.() -> Unit) { + afterDependents = { block(patchContext) } } operator fun Option.invoke() = apply { @@ -90,8 +92,8 @@ sealed class PatchBuilder>( dependencies, compatiblePackages, options, - execute, - finalize, + apply, + afterDependents, type, ) } @@ -101,7 +103,7 @@ class BytecodePatchBuilder private constructor( ) : PatchBuilder( 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().apply { if (extensionInputStream != null) extendWith(extensionInputStream) } @@ -109,7 +111,7 @@ class BytecodePatchBuilder private constructor( ) { internal constructor() : this(null) - fun extendWith(extension: String) = apply { + fun extendWith(extension: String) { // Should be the classloader which loaded the patch class. 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. */ 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]. */ 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) /** diff --git a/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt index f6dd23b..a6dfdd6 100644 --- a/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt +++ b/patcher/src/commonMain/kotlin/com/android/tools/smali/dexlib2/mutable/MutableMethod.kt @@ -12,8 +12,8 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method { private var accessFlags = method.accessFlags private var returnType = method.returnType - // 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) } } + // TODO: Create own mutable MethodImplementation (due to not being able to change members like register count). + private var implementation = method.implementation?.let(::MutableMethodImplementation) 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 _parameterTypes by lazy { method.parameterTypes.toMutableList() } @@ -35,6 +35,10 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method { this.returnType = returnType } + fun setImplementation(implementation: MutableMethodImplementation?) { + this.implementation = implementation + } + override fun getDefiningClass() = definingClass override fun getName() = name @@ -51,7 +55,7 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method { override fun getParameters() = _parameters - override fun getImplementation() = _implementation + override fun getImplementation() = implementation companion object { fun Method.toMutable() = MutableMethod(this) diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/FingerprintTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/FingerprintTest.kt new file mode 100644 index 0000000..48b3ab1 --- /dev/null +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/FingerprintTest.kt @@ -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." + ) + } + } +} diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt index 3bb5227..508f370 100644 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/PatcherTest.kt @@ -5,154 +5,87 @@ import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertAll import org.junit.jupiter.api.assertThrows import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class PatcherTest : PatcherTestBase() { @BeforeAll - fun setUp() = setUpMock() + fun setup() = setupMock() @Test - fun `executes patches in correct order`() { - val executed = mutableListOf() + fun `applies patches in correct order`() { + val applied = mutableListOf() - val patches = setOf( - bytecodePatch { execute { executed += "1" } }, - bytecodePatch { - dependsOn( - bytecodePatch { - execute { executed += "2" } - finalize { executed += "-2" } - }, - bytecodePatch { execute { executed += "3" } }, - ) + infix fun Patch.resultsIn(equals: List) = this to equals + infix fun Pair>.because(reason: String) { + runCatching { setOf(first)() } - execute { executed += "4" } - finalize { executed += "-1" } - }, - ) + assertEquals(second, applied, reason) - assert(executed.isEmpty()) - - patches() - - assertEquals( - listOf("1", "2", "3", "4", "-1", "-2"), - executed, - "Expected patches to be executed in correct order.", - ) - } - - @Test - fun `handles execution of patches correctly when exceptions occur`() { - val executed = mutableListOf() - - infix fun Patch.resultsIn(equals: List) { - 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() + applied.clear() } - // No patches execute successfully, - // because the dependency patch throws an exception inside the execute block. bytecodePatch { dependsOn( bytecodePatch { - execute { throw PatchException("1") } - finalize { executed += "-2" } + apply { applied += "1" } + 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 { dependsOn( bytecodePatch { - execute { executed += "1" } - finalize { executed += "-2" } + apply { throw PatchException("1") } + 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 { dependsOn( bytecodePatch { - execute { executed += "1" } - finalize { throw PatchException("-2") } + apply { applied += "1" } + 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 { dependsOn( bytecodePatch { - execute { executed += "1" } - finalize { executed += "-2" } + apply { applied += "1" } + afterDependents { applied += "-2" } }, ) - - execute { executed += "2" } - finalize { throw PatchException("-1") } - } resultsIn listOf("1", "2", "-2") + apply { applied += "2" } + afterDependents { throw PatchException("-1") } + } resultsIn listOf("1", "2", "-2") because + "afterDependents of a patch should be called " + + "regardless of dependant patches failing." } @Test fun `throws if unmatched fingerprint match is used`() { with(bytecodePatchContext) { - val fingerprint = fingerprint { - strings("doesnt exist") - } + val fingerprint = fingerprint { strings("doesnt exist") } assertThrows("Expected an exception because the fingerprint can't match.") { 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) }, - ) - } - } } diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/ExtensionsTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/extensions/MethodExtensionsTest.kt similarity index 90% rename from patcher/src/jvmTest/kotlin/app/revanced/patcher/ExtensionsTest.kt rename to patcher/src/jvmTest/kotlin/app/revanced/patcher/extensions/MethodExtensionsTest.kt index 57e0ba9..fdc524b 100644 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/ExtensionsTest.kt +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/extensions/MethodExtensionsTest.kt @@ -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.Opcode 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.iface.instruction.OneRegisterInstruction 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 kotlin.test.Test import kotlin.test.assertEquals -private object InstructionExtensionsTest { - private lateinit var testMethod: MutableMethod - private lateinit var testMethodImplementation: MutableMethodImplementation +internal class MethodExtensionsTest { + private val testInstructions = (0..9).map { i -> TestInstruction(i) } + private var method = ImmutableMethod( + "TestClass;", + "testMethod", + null, + "V", + AccessFlags.PUBLIC.value, + null, + null, + MutableMethodImplementation(16) + ).toMutable() @BeforeEach - fun createTestMethod() = - ImmutableMethod( - "TestClass;", - "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() } + fun setup() { + method.instructions.clear() + method.addInstructions(testInstructions) + } @Test fun addInstructionsToImplementationIndexed() = @@ -219,11 +218,11 @@ private object InstructionExtensionsTest { // region Helper methods private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) { - testMethodImplementation.apply(block) + method.implementation!!.apply(block) } private fun applyToMethod(block: MutableMethod.() -> Unit) { - testMethod.apply(block) + method.apply(block) } private fun MutableMethodImplementation.assertRegisterIs( diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/OptionsTest.kt similarity index 94% rename from patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt rename to patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/OptionsTest.kt index c5aba42..6a85aba 100644 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/OptionsTest.kt @@ -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.assertThrows 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 { private val externalOption = stringOption("external", "default") @@ -141,4 +144,4 @@ internal object OptionsTest { } private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block) -} +} \ No newline at end of file diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt deleted file mode 100644 index b31eb0f..0000000 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt +++ /dev/null @@ -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. diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt index 04989f3..c7d5ca7 100644 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/patch/PatchTest.kt @@ -1,5 +1,6 @@ package app.revanced.patcher.patch +import kotlin.reflect.jvm.javaField import kotlin.test.Test import kotlin.test.assertEquals @@ -38,7 +39,7 @@ internal object PatchTest { val print by stringOption("print") val custom = option("custom")() - execute { + this.apply { println(print) println(custom.value) } @@ -46,4 +47,39 @@ internal object PatchTest { 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. diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt index 41dfd5d..a5c2d0c 100644 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/util/SmaliTest.kt @@ -1,43 +1,39 @@ package app.revanced.patcher.util -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.Opcode -import com.android.tools.smali.dexlib2.builder.BuilderInstruction +import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction 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.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference -import java.util.* +import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable +import org.junit.jupiter.api.BeforeEach import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -internal object SmaliTest { - @Test - fun `outputs valid instruction`() { - val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction - val have = "const-string v0, \"Test\"".toInstructions().first() +internal class SmaliTest { + val method = ImmutableMethod( + "Ldummy;", + "name", + emptyList(), // parameters + "V", + AccessFlags.PUBLIC.value, + null, + null, + MutableMethodImplementation(1), + ).toMutable() - assertInstructionsEqual(want, have) + + @BeforeEach + fun setup() { + method.instructions.clear() } @Test - fun `supports branching with own branches`() { - val method = createMethod() - val instructionCount = 8 - val instructionIndex = instructionCount - 2 - val targetIndex = instructionIndex - 1 - - method.addInstructions( - arrayOfNulls(instructionCount).also { - Arrays.fill(it, "const/4 v0, 0x0") - }.joinToString("\n"), - ) + fun `own branches work`() { method.addInstructionsWithLabels( - targetIndex, + 0, """ :test const/4 v0, 0x1 @@ -45,14 +41,13 @@ internal object SmaliTest { """, ) - val instruction = method.getInstruction(instructionIndex) + val targetLocationIndex = method.getInstruction(0).target.location.index - assertEquals(targetIndex, instruction.target.location.index) + assertEquals(0, targetLocationIndex, "Label should point to index 0") } @Test - fun `supports branching to outside branches`() { - val method = createMethod() + fun `external branches work`() { val instructionIndex = 3 val labelIndex = 1 @@ -63,10 +58,8 @@ internal object SmaliTest { """, ) - assertEquals(labelIndex, method.newLabel(labelIndex).location.index) - method.addInstructionsWithLabels( - method.implementation!!.instructions.size, + method.instructions.size, """ const/4 v0, 0x1 if-eqz v0, :test @@ -76,29 +69,8 @@ internal object SmaliTest { ) val instruction = method.getInstruction(instructionIndex) - assertTrue(instruction.target.isPlaced, "Label was not placed") + + assertTrue(instruction.target.isPlaced, "Label should be placed") 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) - } } \ No newline at end of file diff --git a/tests/src/commonMain/kotlin/app/revanced/patcher/PatcherTestBase.kt b/tests/src/commonMain/kotlin/app/revanced/patcher/PatcherTestBase.kt index 94d1cdb..bc818d9 100644 --- a/tests/src/commonMain/kotlin/app/revanced/patcher/PatcherTestBase.kt +++ b/tests/src/commonMain/kotlin/app/revanced/patcher/PatcherTestBase.kt @@ -21,7 +21,7 @@ abstract class PatcherTestBase { protected lateinit var bytecodePatchContext: BytecodePatchContext protected lateinit var resourcePatchContext: ResourcePatchContext - protected fun setUpMock( + protected fun setupMock( method: ImmutableMethod = ImmutableMethod( "class", "method", @@ -89,7 +89,7 @@ abstract class PatcherTestBase { protected operator fun Set.invoke() { runCatching { - execute( + apply( bytecodePatchContext, resourcePatchContext ) { }