diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index 8133dc42..9a1e2c4b 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -170,6 +170,8 @@ public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$Meth } public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch { + public final fun getFingerprints ()Ljava/util/Set; + public fun toString ()Ljava/lang/String; } public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder { @@ -377,6 +379,7 @@ public final class app/revanced/patcher/patch/PatchResult { } public final class app/revanced/patcher/patch/RawResourcePatch : app/revanced/patcher/patch/Patch { + public fun toString ()Ljava/lang/String; } public final class app/revanced/patcher/patch/RawResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder { @@ -384,6 +387,7 @@ public final class app/revanced/patcher/patch/RawResourcePatchBuilder : app/reva } public final class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { + public fun toString ()Ljava/lang/String; } public final class app/revanced/patcher/patch/ResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder { diff --git a/docs/2_2_1_fingerprinting.md b/docs/2_2_1_fingerprinting.md index 02908091..3b8cf3f3 100644 --- a/docs/2_2_1_fingerprinting.md +++ b/docs/2_2_1_fingerprinting.md @@ -174,8 +174,8 @@ Once the fingerprint is resolved, the result can be used in the patch: ```kt val patch = bytecodePatch { - // Add a fingerprint and delegate it's result to a variable. - val result by showAdsFingerprint + // Add a fingerprint and delegate its result to a variable. + val result by showAdsFingerprint() execute { val method = result.method diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index 581b5f51..a1e0843d 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -13,11 +13,13 @@ import java.util.logging.Logger @FunctionalInterface interface PatchesConsumer { - fun accept(patches: PatchSet, integrations: Set = emptySet()) + fun accept(patches: Set>, integrations: Set = emptySet()) } @FunctionalInterface -interface PatcherResultSupplier : Supplier, Closeable +interface PatcherResultSupplier : + Supplier, + Closeable @FunctionalInterface interface PatchExecutorFunction : Function> @@ -29,7 +31,9 @@ interface PatchExecutorFunction : Function> */ class Patcher( private val config: PatcherConfig, -) : PatchExecutorFunction, PatchesConsumer, PatcherResultSupplier { +) : PatchExecutorFunction, + PatchesConsumer, + PatcherResultSupplier { private val logger = Logger.getLogger(Patcher::class.java.name) /** @@ -48,7 +52,7 @@ class Patcher( * @param integrations The integrations to add. Must be a DEX file or container of DEX files. */ @Suppress("NAME_SHADOWING") - override fun accept(patches: PatchSet, integrations: Set) { + override fun accept(patches: Set>, integrations: Set) { // region Add patches // Add all patches to the executablePatches set. @@ -83,11 +87,12 @@ class Patcher( } // Check, if integrations need to be merged. - for (patch in patches) + for (patch in patches) { if (patch.anyRecursively { it.requiresIntegrations }) { context.bytecodePatchContext.integrations.merge = true break } + } } // endregion @@ -113,7 +118,7 @@ class Patcher( executedPatches[this]?.let { patchResult -> patchResult.exception ?: return patchResult - return PatchResult(this, PatchException("'$this' failed previously")) + return PatchResult(this, PatchException("The patch '$this' failed previously")) } // Recursively execute all dependency patches. @@ -122,7 +127,7 @@ class Patcher( return PatchResult( this, PatchException( - "'$this' depends on '$dependency' that raised an exception:\n${it.stackTraceToString()}", + "The patch \"$this\" depends on \"$dependency\" which raised an exception:\n${it.stackTraceToString()}", ), ) } @@ -187,7 +192,7 @@ class Patcher( PatchResult( patch, PatchException( - "'$patch' raised an exception while being closed: ${it.stackTraceToString()}", + "The patch \"$patch\" raised an exception: ${it.stackTraceToString()}", result.exception, ), ), @@ -216,18 +221,3 @@ class Patcher( context.resourcePatchContext.get(), ) } - -/** - * An exception thrown by [Patcher]. - * - * @param errorMessage The exception message. - * @param cause The corresponding [Throwable]. - */ -sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) { - constructor(errorMessage: String) : this(errorMessage, null) - - // TODO: Implement circular dependency detection. - class CircularDependencyException internal constructor(dependant: String) : PatcherException( - "Patch '$dependant' causes a circular dependency", - ) -} diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt index a59f753d..5f750a9c 100644 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt @@ -2,7 +2,6 @@ package app.revanced.patcher.fingerprint -import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.fingerprint.LookupMap.Maps.appendParameters import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps @@ -104,20 +103,17 @@ class MethodFingerprint internal constructor( /** * Resolve a [MethodFingerprint] using a list of [MethodClassPair]. * - * @return True if the resolution was successful, false otherwise. + * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. */ fun MethodFingerprint.resolveUsingMethodClassPair(methodClasses: LookupMap.MethodClassList): Boolean { - methodClasses.forEach { classAndMethod -> - if (resolve(context, classAndMethod.first, classAndMethod.second)) return true + methodClasses.forEach { (classDef, method) -> + if (resolve(context, classDef, method)) return true } return false } - val methodsWithSameStrings = methodStringsLookup() - if (methodsWithSameStrings != null) { - if (resolveUsingMethodClassPair(methodsWithSameStrings)) { - return true - } + if (methodStringsLookup()?.let(::resolveUsingMethodClassPair) == true) { + return true } // No strings declared or none matched (partial matches are allowed). @@ -128,43 +124,42 @@ class MethodFingerprint internal constructor( /** * Resolve a [MethodFingerprint] against a [ClassDef]. * - * @param forClass The class on which to resolve the [MethodFingerprint] in. - * @param context The [BytecodePatchContext] to host proxies. - * @return True if the resolution was successful, false otherwise. + * @param forClass The class in which to resolve the [MethodFingerprint]. + * @param context The [BytecodePatchContext] to create mutable proxies. + * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. */ fun resolve( context: BytecodePatchContext, forClass: ClassDef, ): Boolean { - for (method in forClass.methods) + for (method in forClass.methods) { if (resolve(context, method, forClass)) { return true } + } return false } /** * Resolve a [MethodFingerprint] against a [Method]. * - * @param method The class on which to resolve the [MethodFingerprint] in. - * @param forClass The class on which to resolve the [MethodFingerprint]. - * @param context The [BytecodePatchContext] to host proxies. + * @param method The method in which to resolve the [MethodFingerprint]. + * @param classDef The class in which to resolve the [MethodFingerprint]. + * @param context The [BytecodePatchContext] to create mutable proxies. * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. */ fun resolve( context: BytecodePatchContext, method: Method, - forClass: ClassDef, + classDef: ClassDef, ): Boolean { - val methodFingerprint = this - - if (methodFingerprint.result != null) return true + if (result != null) return true - if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) { + if (returnType != null && !method.returnType.startsWith(returnType)) { return false } - if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) { + if (accessFlags != null && accessFlags != method.accessFlags) { return false } @@ -180,28 +175,22 @@ class MethodFingerprint internal constructor( return true } - if (methodFingerprint.parameters != null && - !parametersEqual( - // TODO: parseParameters() - methodFingerprint.parameters, - method.parameterTypes, - ) - ) { + // TODO: parseParameters() + if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) { return false } - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - if (methodFingerprint.custom != null && !methodFingerprint.custom!!(method, forClass)) { + if (custom != null && !custom.invoke(method, classDef)) { return false } - val stringsScanResult: StringsScanResult? = - if (methodFingerprint.strings != null) { + val stringsScanResult = + if (strings != null) { StringsScanResult( buildList { val instructions = method.instructionsOrNull ?: return false - val stringsList = methodFingerprint.strings.toMutableList() + val stringsList = strings.toMutableList() instructions.forEachIndexed { instructionIndex, instruction -> if ( @@ -226,65 +215,60 @@ class MethodFingerprint internal constructor( null } - val patternScanResult = - if (methodFingerprint.opcodes != null) { - method.instructionsOrNull ?: return false + val patternScanResult = if (opcodes != null) { + val instructions = method.instructionsOrNull ?: return false - fun Method.patternScan( - fingerprint: MethodFingerprint, - ): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { - val instructions = instructions - val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanThreshold + fun patternScan(): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { + val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold - val pattern = fingerprint.opcodes!! - val instructionLength = instructions.count() - val patternLength = pattern.count() + val instructionLength = instructions.count() + val patternLength = opcodes.size - for (index in 0 until instructionLength) { - var patternIndex = 0 - var threshold = fingerprintFuzzyPatternScanThreshold + for (index in 0 until instructionLength) { + var patternIndex = 0 + var threshold = fingerprintFuzzyPatternScanThreshold - while (index + patternIndex < instructionLength) { - val originalOpcode = instructions.elementAt(index + patternIndex).opcode - val patternOpcode = pattern.elementAt(patternIndex) + while (index + patternIndex < instructionLength) { + val originalOpcode = instructions.elementAt(index + patternIndex).opcode + val patternOpcode = opcodes.elementAt(patternIndex) - if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { - // Reaching maximum threshold (0) means, - // the pattern does not match to the current instructions. - if (threshold-- == 0) break - } + if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { + // Reaching maximum threshold (0) means, + // the pattern does not match to the current instructions. + if (threshold-- == 0) break + } - if (patternIndex < patternLength - 1) { - // If the entire pattern has not been scanned yet, continue the scan. - patternIndex++ - continue - } - // The pattern is valid. - return MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult( - index, - index + patternIndex, - ) + if (patternIndex < patternLength - 1) { + // If the entire pattern has not been scanned yet, continue the scan. + patternIndex++ + continue } - } - return null + // The entire pattern has been scanned. + return MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult( + index, + index + patternIndex, + ) + } } - method.patternScan(methodFingerprint) ?: return false - } else { - null + return null } - methodFingerprint.result = - MethodFingerprintResult( - method, - forClass, - MethodFingerprintResult.MethodFingerprintScanResult( - patternScanResult, - stringsScanResult, - ), - context, - ) + patternScan() ?: return false + } else { + null + } + + result = MethodFingerprintResult( + method, + classDef, + MethodFingerprintResult.MethodFingerprintScanResult( + patternScanResult, + stringsScanResult, + ), + context, + ) return true } @@ -309,16 +293,16 @@ internal fun Set.resolveUsingLookupMap(context: BytecodePatch /** * Resolve a list of [MethodFingerprint] against a list of [ClassDef]. * - * @param classes The classes on which to resolve the [MethodFingerprint] in. - * @param context The [BytecodePatchContext] to host proxies. - * @return True if the resolution was successful, false otherwise. + * @param classes The classes in which to resolve the [MethodFingerprint]. + * @param context The [BytecodePatchContext] to create mutable proxies. + * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. */ fun Iterable.resolve( context: BytecodePatchContext, classes: Iterable, ) = forEach { fingerprint -> - for (classDef in classes) { - if (fingerprint.resolve(context, classDef)) break + classes.forEach { + if (fingerprint.resolve(context, it)) return@resolve } } diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 9a3d57ae..9cac9aea 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -14,11 +14,6 @@ import java.net.URLClassLoader import java.util.jar.JarFile import kotlin.reflect.KProperty -/** - * A set of [Patch]. - */ -typealias PatchSet = Set> - typealias PackageName = String typealias VersionName = String typealias Package = Pair?> @@ -115,7 +110,7 @@ class BytecodePatch internal constructor( compatiblePackages: Set?, dependencies: Set>, options: Set>, - internal val fingerprints: Set, + val fingerprints: Set, executeBlock: (Patch.(BytecodePatchContext) -> Unit), finalizeBlock: (Patch.(BytecodePatchContext) -> Unit), ) : Patch( @@ -136,6 +131,8 @@ class BytecodePatch internal constructor( } override fun finalize(context: PatcherContext) = finalize(context.bytecodePatchContext) + + override fun toString() = name ?: "BytecodePatch" } /** @@ -178,7 +175,10 @@ class RawResourcePatch internal constructor( finalizeBlock, ) { override fun execute(context: PatcherContext) = execute(context.resourcePatchContext) + override fun finalize(context: PatcherContext) = finalize(context.resourcePatchContext) + + override fun toString() = name ?: "RawResourcePatch" } /** @@ -221,7 +221,10 @@ class ResourcePatch internal constructor( finalizeBlock, ) { override fun execute(context: PatcherContext) = execute(context.resourcePatchContext) + override fun finalize(context: PatcherContext) = finalize(context.resourcePatchContext) + + override fun toString() = name ?: "ResourcePatch" } /** @@ -346,17 +349,17 @@ class BytecodePatchBuilder internal constructor( use: Boolean, requiresIntegrations: Boolean, ) : PatchBuilder(name, description, use, requiresIntegrations) { - internal val fingerprints = mutableSetOf() + private val fingerprints = mutableSetOf() /** * Add the fingerprint to the patch. */ operator fun MethodFingerprint.invoke() = apply { - fingerprints.add(MethodFingerprint(accessFlags, returnType, parameters, opcodes, strings, custom)) + fingerprints.add(this) } operator fun MethodFingerprint.getValue(nothing: Nothing?, property: KProperty<*>) = result - ?: throw PatchException("Can't delegate unresolved fingerprint result to ${property.name}.") + ?: throw PatchException("Cannot delegate unresolved fingerprint result to ${property.name}.") override fun build() = BytecodePatch( name, @@ -534,7 +537,7 @@ sealed class PatchLoader private constructor( patchesFiles: Set, private val getBinaryClassNames: (patchesFile: File) -> List, private val classLoader: ClassLoader, -) : PatchSet by classLoader.loadPatches(patchesFiles.flatMap(getBinaryClassNames)) { +) : Set> by classLoader.loadPatches(patchesFiles.flatMap(getBinaryClassNames)) { /** * A [PatchLoader] for JAR files. * @@ -542,14 +545,15 @@ sealed class PatchLoader private constructor( * * @constructor Create a new [PatchLoader] for JAR files. */ - internal class Jar(patchesFiles: Set) : PatchLoader( - patchesFiles, - { file -> - JarFile(file).entries().toList().filter { it.name.endsWith(".class") } - .map { it.name.substringBeforeLast('.').replace('/', '.') } - }, - URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()), - ) + internal class Jar(patchesFiles: Set) : + PatchLoader( + patchesFiles, + { file -> + JarFile(file).entries().toList().filter { it.name.endsWith(".class") } + .map { it.name.substringBeforeLast('.').replace('/', '.') } + }, + URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()), + ) /** * A [PatchLoader] for [Dex] files. @@ -560,21 +564,22 @@ sealed class PatchLoader private constructor( * * @constructor Create a new [PatchLoader] for [Dex] files. */ - internal class Dex(patchesFiles: Set, 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, - ), - ) + internal class Dex(patchesFiles: Set, 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, + ), + ) // Companion object required for unit tests. private companion object { @@ -619,7 +624,7 @@ sealed class PatchLoader private constructor( * * @return The loaded patches. */ -fun loadPatchesFromJar(patchesFiles: Set): PatchSet = +fun loadPatchesFromJar(patchesFiles: Set): Set> = PatchLoader.Jar(patchesFiles) /** @@ -631,5 +636,5 @@ fun loadPatchesFromJar(patchesFiles: Set): PatchSet = * * @return The loaded patches. */ -fun loadPatchesFromDex(patchesFiles: Set, optimizedDexDirectory: File? = null): PatchSet = +fun loadPatchesFromDex(patchesFiles: Set, optimizedDexDirectory: File? = null): Set> = PatchLoader.Dex(patchesFiles, optimizedDexDirectory) diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index 1da03f3a..5264d14b 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -115,7 +115,7 @@ internal object PatcherTest { ) } } - private operator fun PatchSet.invoke(): List { + private operator fun Set>.invoke(): List { every { patcher.context.executablePatches } returns toMutableSet() return runBlocking { patcher.apply(false).toList() }