chore: Add tests, fix running patches, add DSL for patch loader
This commit is contained in:
parent
8a64ccf250
commit
c5f02e8c28
|
@ -231,6 +231,9 @@ public final class app/revanced/patcher/patch/PatchKt {
|
|||
public static final fun bytecodePatch (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static synthetic fun bytecodePatch$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static synthetic fun bytecodePatch$default (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;)Ljava/util/Set;
|
||||
public static synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;ILjava/lang/Object;)Ljava/util/Set;
|
||||
public static final fun loadPatchesFromJar (Ljava/util/Set;)Ljava/util/Set;
|
||||
public static final fun rawResourcePatch (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
|
||||
public static final fun rawResourcePatch (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
|
||||
public static synthetic fun rawResourcePatch$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
|
||||
|
@ -242,7 +245,7 @@ public final class app/revanced/patcher/patch/PatchKt {
|
|||
}
|
||||
|
||||
public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
|
||||
public synthetic fun <init> (Ljava/lang/ClassLoader;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/Set;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public synthetic fun <init> (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun add (Lapp/revanced/patcher/patch/Patch;)Z
|
||||
public synthetic fun add (Ljava/lang/Object;)Z
|
||||
public fun addAll (Ljava/util/Collection;)Z
|
||||
|
@ -261,15 +264,6 @@ public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, ko
|
|||
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/patch/PatchLoader$Dex : app/revanced/patcher/patch/PatchLoader {
|
||||
public fun <init> ([Ljava/io/File;Ljava/io/File;)V
|
||||
public synthetic fun <init> ([Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/patch/PatchLoader$Jar : app/revanced/patcher/patch/PatchLoader {
|
||||
public fun <init> ([Ljava/io/File;)V
|
||||
}
|
||||
|
||||
public class app/revanced/patcher/patch/PatchOption {
|
||||
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Lkotlin/jvm/functions/Function2;)V
|
||||
public final fun getDefault ()Ljava/lang/Object;
|
||||
|
|
|
@ -49,8 +49,8 @@ dependencies {
|
|||
// Exclude, otherwise the org.w3c.dom API breaks.
|
||||
exclude(group = "xerces", module = "xmlParserAPIs")
|
||||
}
|
||||
|
||||
testImplementation(libs.kotlin.test)
|
||||
testImplementation(libs.mockk)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
|
|
@ -3,6 +3,7 @@ android = "4.1.1.4"
|
|||
apktool-lib = "2.9.3"
|
||||
kotlin = "1.9.22"
|
||||
kotlinx-coroutines-core = "1.7.3"
|
||||
mockk = "1.13.10"
|
||||
multidexlib2 = "3.0.3.r3"
|
||||
smali = "3.0.5"
|
||||
binary-compatibility-validator = "0.14.0"
|
||||
|
@ -14,6 +15,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref =
|
|||
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
||||
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
||||
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
||||
|
|
|
@ -105,106 +105,103 @@ class Patcher(
|
|||
* @param returnOnError If true, [Patcher] will return immediately if a [Patch] fails.
|
||||
* @return A pair of the name of the [Patch] and its [PatchResult].
|
||||
*/
|
||||
override fun apply(returnOnError: Boolean) =
|
||||
flow {
|
||||
fun Patch<*>.execute(
|
||||
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
|
||||
): PatchResult {
|
||||
// If the patch was executed before or failed, return it's the result.
|
||||
executedPatches[this]?.let { patchResult ->
|
||||
patchResult.exception ?: return patchResult
|
||||
override fun apply(returnOnError: Boolean) = flow {
|
||||
fun Patch<*>.execute(
|
||||
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
|
||||
): PatchResult {
|
||||
// If the patch was executed before or failed, return it's the result.
|
||||
executedPatches[this]?.let { patchResult ->
|
||||
patchResult.exception ?: return patchResult
|
||||
|
||||
return PatchResult(this, PatchException("'$this' failed previously"))
|
||||
}
|
||||
|
||||
// Recursively execute all dependency patches.
|
||||
dependencies.forEach { dependency ->
|
||||
execute(executedPatches).exception?.let {
|
||||
return PatchResult(
|
||||
this,
|
||||
PatchException(
|
||||
"'$this' depends on '$dependency' that raised an exception:\n${it.stackTraceToString()}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the patch.
|
||||
return try {
|
||||
execute(context)
|
||||
|
||||
PatchResult(this)
|
||||
} catch (exception: PatchException) {
|
||||
PatchResult(this, exception)
|
||||
} catch (exception: Exception) {
|
||||
PatchResult(this, PatchException(exception))
|
||||
}.also { executedPatches[this] = it }
|
||||
return PatchResult(this, PatchException("'$this' failed previously"))
|
||||
}
|
||||
|
||||
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
|
||||
|
||||
LookupMap.initializeLookupMaps(context.bytecodeContext)
|
||||
|
||||
// Prevent from decoding the app manifest twice if it is not needed.
|
||||
if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
|
||||
context.resourceContext.decodeResources(config.resourceMode)
|
||||
}
|
||||
|
||||
logger.info("Executing patches")
|
||||
|
||||
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>()
|
||||
|
||||
context.executablePatches.sortedBy { it.name }.forEach { patch ->
|
||||
val patchResult = patch.execute(executedPatches)
|
||||
|
||||
// If the patch failed, emit the result, even if it is closeable.
|
||||
// Results of executed patches that are closeable will be emitted later.
|
||||
patchResult.exception?.let {
|
||||
// Propagate exception to caller instead of wrapping it in a new exception.
|
||||
emit(patchResult)
|
||||
|
||||
if (returnOnError) return@flow
|
||||
} ?: run {
|
||||
if (patch is Closeable) return@run
|
||||
|
||||
emit(patchResult)
|
||||
}
|
||||
}
|
||||
|
||||
executedPatches.values.filter { it.exception == null }.asReversed().forEach { executedPatch ->
|
||||
val patch = executedPatch.patch
|
||||
|
||||
val result =
|
||||
try {
|
||||
(patch as Closeable).close()
|
||||
|
||||
executedPatch
|
||||
} catch (exception: PatchException) {
|
||||
PatchResult(patch, exception)
|
||||
} catch (exception: Exception) {
|
||||
PatchResult(patch, PatchException(exception))
|
||||
}
|
||||
|
||||
result.exception?.let {
|
||||
emit(
|
||||
PatchResult(
|
||||
patch,
|
||||
PatchException(
|
||||
"'$patch' raised an exception while being closed: ${it.stackTraceToString()}",
|
||||
result.exception,
|
||||
),
|
||||
// Recursively execute all dependency patches.
|
||||
dependencies.forEach { dependency ->
|
||||
dependency.execute(executedPatches).exception?.let {
|
||||
return PatchResult(
|
||||
this,
|
||||
PatchException(
|
||||
"'$this' depends on '$dependency' that raised an exception:\n${it.stackTraceToString()}",
|
||||
),
|
||||
)
|
||||
|
||||
if (returnOnError) return@flow
|
||||
} ?: run {
|
||||
patch.name ?: return@run
|
||||
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the patch.
|
||||
return try {
|
||||
execute(context)
|
||||
|
||||
PatchResult(this)
|
||||
} catch (exception: PatchException) {
|
||||
PatchResult(this, exception)
|
||||
} catch (exception: Exception) {
|
||||
PatchResult(this, PatchException(exception))
|
||||
}.also { executedPatches[this] = it }
|
||||
}
|
||||
|
||||
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
|
||||
|
||||
LookupMap.initializeLookupMaps(context.bytecodeContext)
|
||||
|
||||
// Prevent from decoding the app manifest twice if it is not needed.
|
||||
if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
|
||||
context.resourceContext.decodeResources(config.resourceMode)
|
||||
}
|
||||
|
||||
logger.info("Executing patches")
|
||||
|
||||
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>()
|
||||
|
||||
context.executablePatches.sortedBy { it.name }.forEach { patch ->
|
||||
val patchResult = patch.execute(executedPatches)
|
||||
|
||||
// If the patch failed, emit the result, even if it is closeable.
|
||||
// Results of executed patches that are closeable will be emitted later.
|
||||
patchResult.exception?.let {
|
||||
// Propagate exception to caller instead of wrapping it in a new exception.
|
||||
emit(patchResult)
|
||||
|
||||
if (returnOnError) return@flow
|
||||
} ?: run {
|
||||
emit(patchResult)
|
||||
}
|
||||
}
|
||||
|
||||
executedPatches.values.filter { it.exception == null }.asReversed().forEach { executionResult ->
|
||||
val patch = executionResult.patch
|
||||
|
||||
val result =
|
||||
try {
|
||||
patch.finalize(context)
|
||||
|
||||
executionResult
|
||||
} catch (exception: PatchException) {
|
||||
PatchResult(patch, exception)
|
||||
} catch (exception: Exception) {
|
||||
PatchResult(patch, PatchException(exception))
|
||||
}
|
||||
|
||||
result.exception?.let {
|
||||
emit(
|
||||
PatchResult(
|
||||
patch,
|
||||
PatchException(
|
||||
"'$patch' raised an exception while being closed: ${it.stackTraceToString()}",
|
||||
result.exception,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
if (returnOnError) return@flow
|
||||
} ?: run {
|
||||
patch.name ?: return@run
|
||||
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() = LookupMap.clearLookupMaps()
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,11 +29,11 @@ annotation class FuzzyPatternScanMethod(
|
|||
/**
|
||||
* A fingerprint to resolve methods.
|
||||
*
|
||||
* @param returnType The method's return type compared using [String.startsWith].
|
||||
* @param accessFlags The method's exact access flags using values of [AccessFlags].
|
||||
* @param returnType The return type compared using [String.startsWith].
|
||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
||||
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
|
||||
* @param opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by `null`.
|
||||
* @param strings A list of the method's strings compared each using [String.contains].
|
||||
* @param opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
|
||||
* @param strings A list of the strings compared each using [String.contains].
|
||||
* @param custom A custom condition for this fingerprint.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
|
@ -312,12 +312,8 @@ class MethodFingerprint(
|
|||
* - Faster: Specify [MethodFingerprint.accessFlags], [MethodFingerprint.returnType] and [MethodFingerprint.parameters].
|
||||
* - Fastest: Specify [MethodFingerprint.strings], with at least one string being an exact (non-partial) match.
|
||||
*/
|
||||
internal fun Set<MethodFingerprint>.resolveUsingLookupMap(context: BytecodePatchContext) {
|
||||
if (methods.isEmpty()) throw PatchException("lookup map not initialized")
|
||||
|
||||
forEach { fingerprint ->
|
||||
fingerprint.resolveUsingLookupMap(context)
|
||||
}
|
||||
internal fun Set<MethodFingerprint>.resolveUsingLookupMap(context: BytecodePatchContext) = forEach { fingerprint ->
|
||||
fingerprint.resolveUsingLookupMap(context)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -410,11 +406,11 @@ class MethodFingerprintResult(
|
|||
/**
|
||||
* A builder for [MethodFingerprint].
|
||||
*
|
||||
* @property returnType The method's return type compared using [String.startsWith].
|
||||
* @property accessFlags The method's exact access flags using values of [AccessFlags].
|
||||
* @property returnType The return type compared using [String.startsWith].
|
||||
* @property accessFlags The exact access flags using values of [AccessFlags].
|
||||
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
|
||||
* @property opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by `null`.
|
||||
* @property strings A list of the method's strings compared each using [String.contains].
|
||||
* @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.
|
||||
*
|
||||
* @constructor Creates a new [MethodFingerprintBuilder].
|
||||
|
@ -428,34 +424,34 @@ class MethodFingerprintBuilder internal constructor() {
|
|||
private var customBlock: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null
|
||||
|
||||
/**
|
||||
* Set the method's return type.
|
||||
* Set the return type.
|
||||
*
|
||||
* @param returnType The method's return type compared using [String.startsWith].
|
||||
* @param returnType The return type compared using [String.startsWith].
|
||||
*/
|
||||
fun returns(returnType: String) {
|
||||
infix fun returns(returnType: String) {
|
||||
this.returnType = returnType
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the method's access flags.
|
||||
* Set the access flags.
|
||||
*
|
||||
* @param accessFlags The method's exact access flags using values of [AccessFlags].
|
||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
||||
*/
|
||||
fun accessFlags(accessFlags: Int) {
|
||||
this.accessFlags = accessFlags
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the method's access flags.
|
||||
* Set the access flags.
|
||||
*
|
||||
* @param accessFlags The method's exact access flags using values of [AccessFlags].
|
||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
||||
*/
|
||||
fun accessFlags(accessFlags: AccessFlags) {
|
||||
this.accessFlags = accessFlags.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the method's parameters.
|
||||
* Set the parameters.
|
||||
*
|
||||
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
|
||||
*/
|
||||
|
@ -464,9 +460,9 @@ class MethodFingerprintBuilder internal constructor() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the method's opcodes.
|
||||
* Set the opcodes.
|
||||
*
|
||||
* @param opcodes An opcode pattern of the method's instructions.
|
||||
* @param opcodes An opcode pattern of instructions.
|
||||
* Wildcard or unknown opcodes can be specified by `null`.
|
||||
*/
|
||||
fun opcodes(vararg opcodes: Opcode?) {
|
||||
|
@ -474,9 +470,9 @@ class MethodFingerprintBuilder internal constructor() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the method's opcodes.
|
||||
* Set the opcodes.
|
||||
*
|
||||
* @param instructions A list of the method's instructions or opcode names in SMALI format.
|
||||
* @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.
|
||||
|
@ -497,9 +493,9 @@ class MethodFingerprintBuilder internal constructor() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set the method's strings.
|
||||
* Set the strings.
|
||||
*
|
||||
* @param strings A list of the method's strings compared each using [String.contains].
|
||||
* @param strings A list of strings compared each using [String.contains].
|
||||
*/
|
||||
fun strings(vararg strings: String) {
|
||||
this.strings = strings.toList()
|
||||
|
@ -539,4 +535,4 @@ fun methodFingerprint(block: MethodFingerprintBuilder.() -> Unit) =
|
|||
* @return The created [MethodFingerprint].
|
||||
*/
|
||||
fun BytecodePatchBuilder.methodFingerprint(block: MethodFingerprintBuilder.() -> Unit) =
|
||||
MethodFingerprintBuilder().apply(block).build()() // Invoke to add to its set of fingerprints.
|
||||
app.revanced.patcher.fingerprint.methodFingerprint(block)() // Invoke to add to its set of fingerprints.
|
||||
|
|
|
@ -257,17 +257,17 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||
protected var finalizeBlock: ((C) -> Unit) = { }
|
||||
|
||||
/**
|
||||
* Adds a compatible packages to the patch.
|
||||
* Add a compatible package to the patch.
|
||||
*
|
||||
* @param versions The versions of the package.
|
||||
*/
|
||||
operator fun String.invoke(vararg versions: String) {
|
||||
if (compatiblePackages == null) compatiblePackages = mutableSetOf()
|
||||
compatiblePackages!!.add(this to versions.toSet())
|
||||
compatiblePackages!! += this to versions.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the patch as a dependency.
|
||||
* Add the patch as a dependency.
|
||||
*
|
||||
* @return The added patch.
|
||||
*/
|
||||
|
@ -276,7 +276,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||
}
|
||||
|
||||
/**
|
||||
* Adds options to the patch.
|
||||
* Add options to the patch.
|
||||
*
|
||||
* @param option The options to add.
|
||||
*/
|
||||
|
@ -285,7 +285,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the execution block of the patch.
|
||||
* Set the execution block of the patch.
|
||||
*
|
||||
* @param block The execution block of the patch.
|
||||
*/
|
||||
|
@ -294,7 +294,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the finalizing block of the patch.
|
||||
* Set the finalizing block of the patch.
|
||||
*
|
||||
* @param block The closing block of the patch.
|
||||
*/
|
||||
|
@ -339,7 +339,7 @@ class BytecodePatchBuilder internal constructor(
|
|||
}
|
||||
|
||||
operator fun MethodFingerprint.getValue(nothing: Nothing?, property: KProperty<*>) = result
|
||||
?: throw PatchException("Failed to resolve ${this.javaClass.simpleName}")
|
||||
?: throw PatchException("Can't delegate unresolved fingerprint result to ${property.name}.")
|
||||
|
||||
override fun build() = BytecodePatch(
|
||||
name,
|
||||
|
@ -446,7 +446,7 @@ fun bytecodePatch(
|
|||
) = BytecodePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block) as BytecodePatch
|
||||
|
||||
/**
|
||||
* Creates a new [BytecodePatch] and adds it to the patch.
|
||||
* Create a new [BytecodePatch] and add it to the patch.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
|
@ -466,7 +466,7 @@ fun PatchBuilder<*>.bytecodePatch(
|
|||
) = BytecodePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block)() as BytecodePatch
|
||||
|
||||
/**
|
||||
* Creates a new [RawResourcePatch].
|
||||
* Create a new [RawResourcePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
|
@ -485,7 +485,7 @@ fun rawResourcePatch(
|
|||
) = RawResourcePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block) as RawResourcePatch
|
||||
|
||||
/**
|
||||
* Creates a new [RawResourcePatch] and adds it to the patch.
|
||||
* Create a new [RawResourcePatch] and add it to the patch.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
|
@ -505,7 +505,7 @@ fun PatchBuilder<*>.rawResourcePatch(
|
|||
) = RawResourcePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block)() as RawResourcePatch
|
||||
|
||||
/**
|
||||
* Creates a new [ResourcePatch].
|
||||
* Create a new [ResourcePatch].
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
|
@ -525,7 +525,7 @@ fun resourcePatch(
|
|||
) = ResourcePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block) as ResourcePatch
|
||||
|
||||
/**
|
||||
* Creates a new [ResourcePatch] and adds it to the patch.
|
||||
* Create a new [ResourcePatch] and add it to the patch.
|
||||
*
|
||||
* @param name The name of the patch.
|
||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||
|
@ -568,20 +568,19 @@ class PatchResult internal constructor(val patch: Patch<*>, val exception: Patch
|
|||
* Loads patches from JAR or DEX files declared as public fields or properties.
|
||||
* Patches with no name are not loaded.
|
||||
*
|
||||
* @param classLoader The [ClassLoader] to use for loading the classes.
|
||||
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
|
||||
* @param patchesFiles A set of JAR or DEX files to load the patches from.
|
||||
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
|
||||
* @param classLoader The [ClassLoader] to use for loading the classes.
|
||||
*/
|
||||
sealed class PatchLoader private constructor(
|
||||
classLoader: ClassLoader,
|
||||
patchesFiles: Set<File>,
|
||||
getBinaryClassNames: (patchesFile: File) -> List<String>,
|
||||
// This constructor parameter is unfortunately necessary,
|
||||
// so that a reference to the mutable set is present in the constructor to be able to add patches to it.
|
||||
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
|
||||
private val patchSet: MutableSet<Patch<*>> = mutableSetOf(),
|
||||
) : PatchSet by patchSet {
|
||||
private val getBinaryClassNames: (patchesFile: File) -> List<String>,
|
||||
private val classLoader: ClassLoader,
|
||||
) : PatchSet by mutableSetOf() {
|
||||
init {
|
||||
@Suppress("UNCHECKED_CAST", "LeakingThis")
|
||||
val thisSet = this as MutableSet<Patch<*>>
|
||||
|
||||
patchesFiles.asSequence().flatMap(getBinaryClassNames).map {
|
||||
classLoader.loadClass(it)
|
||||
}.flatMap {
|
||||
|
@ -598,9 +597,7 @@ sealed class PatchLoader private constructor(
|
|||
patchFields + patchMethods
|
||||
}.filter {
|
||||
it.name != null
|
||||
}.toList().let { patches ->
|
||||
patchSet.addAll(patches)
|
||||
}
|
||||
}.toList().let(thisSet::addAll)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -608,15 +605,15 @@ sealed class PatchLoader private constructor(
|
|||
*
|
||||
* @param patchesFiles The JAR files to load the patches from.
|
||||
*
|
||||
* @constructor Creates a new [PatchLoader] for JAR files.
|
||||
* @constructor Create a new [PatchLoader] for JAR files.
|
||||
*/
|
||||
class Jar(vararg patchesFiles: File) : PatchLoader(
|
||||
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
|
||||
patchesFiles.toSet(),
|
||||
internal class Jar(patchesFiles: Set<File>) : PatchLoader(
|
||||
patchesFiles,
|
||||
{ file ->
|
||||
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
|
||||
.map { it.name.substringBeforeLast('.').replace('/', '.') }
|
||||
},
|
||||
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -626,21 +623,41 @@ sealed class PatchLoader private constructor(
|
|||
* @param optimizedDexDirectory The directory to store optimized DEX files in.
|
||||
* This parameter is deprecated and has no effect since API level 26.
|
||||
*
|
||||
* @constructor Creates a new [PatchLoader] for [Dex] files.
|
||||
* @constructor Create a new [PatchLoader] for [Dex] files.
|
||||
*/
|
||||
class Dex(vararg patchesFiles: File, optimizedDexDirectory: File? = null) : PatchLoader(
|
||||
DexClassLoader(
|
||||
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||
optimizedDexDirectory?.absolutePath,
|
||||
null,
|
||||
PatchLoader::class.java.classLoader,
|
||||
),
|
||||
patchesFiles.toSet(),
|
||||
internal class Dex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) : PatchLoader(
|
||||
patchesFiles,
|
||||
{ patchBundle ->
|
||||
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
|
||||
.map { classDef ->
|
||||
classDef.type.substring(1, classDef.length - 1)
|
||||
}
|
||||
},
|
||||
DexClassLoader(
|
||||
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||
optimizedDexDirectory?.absolutePath,
|
||||
null,
|
||||
PatchLoader::class.java.classLoader,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads patches from JAR files.
|
||||
*
|
||||
* @param patchesFiles The JAR files to load the patches from.
|
||||
*
|
||||
* @return The loaded patches.
|
||||
*/
|
||||
fun loadPatchesFromJar(patchesFiles: Set<File>): PatchSet =
|
||||
PatchLoader.Jar(patchesFiles)
|
||||
|
||||
/**
|
||||
* Loads patches from DEX files.
|
||||
*
|
||||
* @param patchesFiles The DEX files to load the patches from.
|
||||
*
|
||||
* @return The loaded patches.
|
||||
*/
|
||||
fun loadPatchesFromDex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null): PatchSet =
|
||||
PatchLoader.Dex(patchesFiles, optimizedDexDirectory)
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.fingerprint.methodFingerprint
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchSet
|
||||
import app.revanced.patcher.patch.ResourcePatchContext
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.ProxyClassList
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import java.util.logging.Logger
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
object PatcherTest {
|
||||
private lateinit var patcher: Patcher
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
patcher = mockk<Patcher> {
|
||||
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
|
||||
setPrivateField(
|
||||
"config",
|
||||
mockk<PatcherConfig> {
|
||||
every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE
|
||||
},
|
||||
)
|
||||
setPrivateField(
|
||||
"logger",
|
||||
Logger.getAnonymousLogger(),
|
||||
)
|
||||
|
||||
every { context.bytecodeContext.classes } returns mockk(relaxed = true)
|
||||
every { context.bytecodeContext.integrations } returns mockk(relaxed = true)
|
||||
every { apply(false) } answers { callOriginal() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executes patches in correct order`() {
|
||||
val executed = mutableListOf<String>()
|
||||
|
||||
val patches = setOf(
|
||||
bytecodePatch { execute { executed += "1" } },
|
||||
bytecodePatch {
|
||||
// Directly depend on this patch.
|
||||
bytecodePatch {
|
||||
execute { executed += "2" }
|
||||
finalize { executed += "-2" }
|
||||
}
|
||||
|
||||
// Manually depend on this patch.
|
||||
app.revanced.patcher.patch.bytecodePatch { execute { executed += "3" } }()
|
||||
|
||||
execute { executed += "4" }
|
||||
finalize { executed += "-1" }
|
||||
},
|
||||
)
|
||||
|
||||
assert(executed.isEmpty())
|
||||
|
||||
patches()
|
||||
|
||||
assertEquals(
|
||||
listOf("1", "2", "3", "4", "-1", "-2"),
|
||||
executed,
|
||||
"Expected patches to be executed in correct order.",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws if unresolved fingerprint result is delegated`() {
|
||||
val patches = setOf(
|
||||
bytecodePatch {
|
||||
// Fingerprint can never be resolved.
|
||||
val result by methodFingerprint {}
|
||||
// Manually add the fingerprint.
|
||||
app.revanced.patcher.fingerprint.methodFingerprint { }()
|
||||
|
||||
execute {
|
||||
// Throws, because the fingerprint can't be resolved.
|
||||
result.scanResult
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assertEquals(2, patches.first().fingerprints.size)
|
||||
|
||||
val results = patches()
|
||||
|
||||
assertTrue(
|
||||
results.any { it.exception != null },
|
||||
"Expected an exception because the fingerprint can't be resolved.",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolves fingerprint`() {
|
||||
mockClassWithMethod()
|
||||
|
||||
val patches = setOf(bytecodePatch { methodFingerprint { this returns "V" }() })
|
||||
|
||||
assertNull(
|
||||
patches.first().fingerprints.first().result,
|
||||
"Expected fingerprint to be unresolved before execution.",
|
||||
)
|
||||
|
||||
patches()
|
||||
|
||||
assertDoesNotThrow("Expected fingerprint to be resolved.") {
|
||||
assertEquals(
|
||||
"V",
|
||||
patches.first().fingerprints.first().result!!.method.returnType,
|
||||
"Expected fingerprint to be resolved.",
|
||||
)
|
||||
}
|
||||
}
|
||||
private operator fun PatchSet.invoke(): List<PatchResult> {
|
||||
every { patcher.context.executablePatches } returns toMutableSet()
|
||||
|
||||
return runBlocking { patcher.apply(false).toList() }
|
||||
}
|
||||
|
||||
private fun Any.setPrivateField(field: String, value: Any) {
|
||||
this::class.java.getDeclaredField(field).apply {
|
||||
this.isAccessible = true
|
||||
set(this@setPrivateField, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun mockClassWithMethod() {
|
||||
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
|
||||
mutableSetOf(
|
||||
ImmutableClassDef(
|
||||
"class",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
listOf(
|
||||
ImmutableMethod(
|
||||
"class",
|
||||
"method",
|
||||
emptyList(),
|
||||
"V",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue