1
mirror of https://github.com/revanced/revanced-patcher synced 2025-10-01 11:30:50 +02:00

Compare commits

..

13 Commits

Author SHA1 Message Date
semantic-release-bot
b369a30dd5 chore(release): 1.0.0-dev.17 [skip ci]
# [1.0.0-dev.17](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.16...v1.0.0-dev.17) (2022-05-31)

### Features

* patch dependencies annotation and `PatcherOptions` ([8442991](8442991290))
2022-05-31 23:41:12 +00:00
oSumAtrIX
8442991290 feat: patch dependencies annotation and PatcherOptions 2022-06-01 01:33:30 +02:00
semantic-release-bot
a06c0db6a7 chore(release): 1.0.0-dev.16 [skip ci]
# [1.0.0-dev.16](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.15...v1.0.0-dev.16) (2022-05-27)

### Bug Fixes

* `JarPatchBundle` loading non-class files to class loader ([3f0c740](3f0c740200))
* remove dependency to fork of Apktool ([0fa529f](0fa529fcdf))

### Features

* migrate to `DexPatchBundle` and `JarPatchBundle` ([7573db2](7573db2575))
2022-05-27 12:30:13 +00:00
oSumAtrIX
3f0c740200 fix: JarPatchBundle loading non-class files to class loader 2022-05-27 14:26:06 +02:00
oSumAtrIX
545c5c144d chore: update gradlew wrapper 2022-05-26 03:52:28 +02:00
oSumAtrIX
0fa529fcdf fix: remove dependency to fork of Apktool 2022-05-26 03:51:25 +02:00
oSumAtrIX
7573db2575 feat: migrate to DexPatchBundle and JarPatchBundle
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-05-26 01:02:41 +02:00
semantic-release-bot
70ca184cf9 chore(release): 1.0.0-dev.15 [skip ci]
# [1.0.0-dev.15](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.14...v1.0.0-dev.15) (2022-05-25)

### Features

* utility functions to get metadata of patch & sigs ([72f16b7](72f16b7785))
2022-05-25 20:55:57 +00:00
Lucaskyy
72f16b7785 feat: utility functions to get metadata of patch & sigs 2022-05-25 22:54:20 +02:00
Lucaskyy
fc03639b26 chore: fix typo 2022-05-25 22:52:57 +02:00
semantic-release-bot
88a85f94e7 chore(release): 1.0.0-dev.14 [skip ci]
# [1.0.0-dev.14](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.13...v1.0.0-dev.14) (2022-05-24)

### Bug Fixes

* reformat (trigger release) ([45a167e](45a167e785))
2022-05-24 18:12:32 +00:00
Lucaskyy
45a167e785 fix: reformat (trigger release) 2022-05-24 20:11:04 +02:00
Lucaskyy
699d8abf59 refactor: use apktool fork
also fixed some compilation issues
2022-05-24 17:43:43 +02:00
25 changed files with 486 additions and 329 deletions

View File

@@ -1,5 +1,6 @@
name: Release name: Release
on: on:
workflow_dispatch:
push: push:
branches: branches:
- main - main

View File

@@ -1,3 +1,37 @@
# [1.0.0-dev.17](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.16...v1.0.0-dev.17) (2022-05-31)
### Features
* patch dependencies annotation and `PatcherOptions` ([8442991](https://github.com/revanced/revanced-patcher/commit/84429912900872405b44804943357dda8430a550))
# [1.0.0-dev.16](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.15...v1.0.0-dev.16) (2022-05-27)
### Bug Fixes
* `JarPatchBundle` loading non-class files to class loader ([3f0c740](https://github.com/revanced/revanced-patcher/commit/3f0c740200dd91a060426638c2f8f516938b4c53))
* remove dependency to fork of Apktool ([0fa529f](https://github.com/revanced/revanced-patcher/commit/0fa529fcdf9a7b5ea9a361b9f9f32f3f3fce009f))
### Features
* migrate to `DexPatchBundle` and `JarPatchBundle` ([7573db2](https://github.com/revanced/revanced-patcher/commit/7573db25757de89824af4f3aea167e500120eabb))
# [1.0.0-dev.15](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.14...v1.0.0-dev.15) (2022-05-25)
### Features
* utility functions to get metadata of patch & sigs ([72f16b7](https://github.com/revanced/revanced-patcher/commit/72f16b778587c28d8f8e91da502f197e7dc35d6d))
# [1.0.0-dev.14](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.13...v1.0.0-dev.14) (2022-05-24)
### Bug Fixes
* reformat (trigger release) ([45a167e](https://github.com/revanced/revanced-patcher/commit/45a167e7856da0306f796953775c7b7543d9bec0))
# [1.0.0-dev.13](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.12...v1.0.0-dev.13) (2022-05-24) # [1.0.0-dev.13](https://github.com/revanced/revanced-patcher/compare/v1.0.0-dev.12...v1.0.0-dev.13) (2022-05-24)

View File

@@ -15,22 +15,24 @@ repositories {
// Instead, you should set them in: // Instead, you should set them in:
// Windows: %homepath%\.gradle\gradle.properties // Windows: %homepath%\.gradle\gradle.properties
// Linux: ~/.gradle/gradle.properties // Linux: ~/.gradle/gradle.properties
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") // DO NOT CHANGE! username =
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE! project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") // DO NOT CHANGE!
password =
project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
} }
} }
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.21") implementation(kotlin("stdlib"))
api("xpp3:xpp3:1.1.4c") api("xpp3:xpp3:1.1.4c")
api("org.apktool:apktool-lib:2.6.1") api("org.apktool:apktool-lib:2.6.1")
api("app.revanced:multidexlib2:2.5.2.r2") api("app.revanced:multidexlib2:2.5.2.r2")
api("org.smali:smali:2.5.2") api("org.smali:smali:2.5.2")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.6.21") testImplementation(kotlin("test"))
implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.21") implementation(kotlin("reflect"))
} }
tasks { tasks {

View File

@@ -1,2 +1,2 @@
kotlin.code.style = official kotlin.code.style = official
version = 1.0.0-dev.13 version = 1.0.0-dev.17

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

269
gradlew vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,17 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.data.PatcherData import app.revanced.patcher.data.PatcherData
import app.revanced.patcher.data.base.Data import app.revanced.patcher.data.base.Data
import app.revanced.patcher.data.implementation.findIndexed import app.revanced.patcher.data.implementation.findIndexed
import app.revanced.patcher.extensions.findAnnotationRecursively import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.nullOutputStream
import app.revanced.patcher.patch.base.Patch import app.revanced.patcher.patch.base.Patch
import app.revanced.patcher.patch.implementation.BytecodePatch import app.revanced.patcher.patch.implementation.BytecodePatch
import app.revanced.patcher.patch.implementation.ResourcePatch import app.revanced.patcher.patch.implementation.ResourcePatch
import app.revanced.patcher.patch.implementation.misc.PatchResult
import app.revanced.patcher.patch.implementation.misc.PatchResultError
import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess
import app.revanced.patcher.signature.implementation.method.MethodSignature
import app.revanced.patcher.signature.implementation.method.resolver.MethodSignatureResolver import app.revanced.patcher.signature.implementation.method.resolver.MethodSignatureResolver
import app.revanced.patcher.util.ListBackedSet import app.revanced.patcher.util.ListBackedSet
import brut.androlib.Androlib import brut.androlib.Androlib
@@ -28,20 +30,15 @@ import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.DexFile import org.jf.dexlib2.iface.DexFile
import org.jf.dexlib2.writer.io.MemoryDataStore import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.File import java.io.File
import java.io.OutputStream
val NAMER = BasicDexFileNamer() val NAMER = BasicDexFileNamer()
/** /**
* The ReVanced Patcher. * The ReVanced Patcher.
* @param inputFile The input file (usually an apk file). * @param options The options for the patcher.
* @param resourceCacheDirectory Directory to cache resources.
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.
*/ */
class Patcher( class Patcher(
inputFile: File, private val options: PatcherOptions
// TODO: maybe a file system in memory is better. Could cause high memory usage.
private val resourceCacheDirectory: String, private val patchResources: Boolean = false
) { ) {
val packageVersion: String val packageVersion: String
val packageName: String val packageName: String
@@ -49,12 +46,10 @@ class Patcher(
private lateinit var usesFramework: UsesFramework private lateinit var usesFramework: UsesFramework
private val patcherData: PatcherData private val patcherData: PatcherData
private val opcodes: Opcodes private val opcodes: Opcodes
private var signaturesResolved = false
init { init {
val extFileInput = ExtFile(inputFile) val extFileInput = ExtFile(options.inputFile)
val outDir = File(resourceCacheDirectory) val outDir = File(options.resourceCacheDirectory)
if (outDir.exists()) outDir.deleteRecursively() if (outDir.exists()) outDir.deleteRecursively()
outDir.mkdir() outDir.mkdir()
@@ -63,7 +58,7 @@ class Patcher(
val androlib = Androlib() val androlib = Androlib()
val resourceTable = androlib.getResTable(extFileInput, true) val resourceTable = androlib.getResTable(extFileInput, true)
if (patchResources) { if (options.patchResources) {
// 1. decode resources to cache directory // 1. decode resources to cache directory
androlib.decodeManifestWithResources(extFileInput, outDir, resourceTable) androlib.decodeManifestWithResources(extFileInput, outDir, resourceTable)
androlib.decodeResourcesFull(extFileInput, outDir, resourceTable) androlib.decodeResourcesFull(extFileInput, outDir, resourceTable)
@@ -85,7 +80,7 @@ class Patcher(
XmlPullStreamDecoder( XmlPullStreamDecoder(
axmlParser, AndrolibResources().resXmlSerializer axmlParser, AndrolibResources().resXmlSerializer
).decodeManifest( ).decodeManifest(
extFileInput.directory.getFileInput("AndroidManifest.xml"), OutputStream.nullOutputStream() extFileInput.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
) )
} }
@@ -93,11 +88,11 @@ class Patcher(
packageVersion = resourceTable.versionInfo.versionName packageVersion = resourceTable.versionInfo.versionName
packageName = resourceTable.currentResPackage.name packageName = resourceTable.currentResPackage.name
// read dex files // read dex files
val dexFile = MultiDexIO.readDexFile(true, inputFile, NAMER, null, null) val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
opcodes = dexFile.opcodes opcodes = dexFile.opcodes
// save to patcher data // save to patcher data
patcherData = PatcherData(dexFile.classes.toMutableList(), resourceCacheDirectory) patcherData = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory)
} }
/** /**
@@ -144,8 +139,8 @@ class Patcher(
} }
// build modified resources // build modified resources
if (patchResources) { if (options.patchResources) {
val extDir = ExtFile(resourceCacheDirectory) val extDir = ExtFile(options.resourceCacheDirectory)
// TODO: figure out why a new instance of Androlib is necessary here // TODO: figure out why a new instance of Androlib is necessary here
Androlib().buildResources(extDir, usesFramework) Androlib().buildResources(extDir, usesFramework)
@@ -161,33 +156,62 @@ class Patcher(
} }
/** /**
* Add a patch to the patcher. * Add [Patch]es to the patcher.
* @param patches The patches to add. * @param patches [Patch]es The patches to add.
*/ */
fun addPatches(patches: Iterable<Patch<Data>>) { fun addPatches(patches: Iterable<Class<out Patch<Data>>>) {
patcherData.patches.addAll(patches) patcherData.patches.addAll(patches)
} }
/** /**
* Resolves all signatures. * Apply a [patch] and its dependencies recursively.
* @param patch The [patch] to apply.
* @param appliedPatches A list of [patch] names, to prevent applying [patch]es twice.
* @return The result of executing the [patch].
*/ */
fun resolveSignatures(): List<MethodSignature> { private fun applyPatch(
val signatures = buildList { patch: Class<out Patch<Data>>, appliedPatches: MutableList<String>
for (patch in patcherData.patches) { ): PatchResult {
if (patch !is BytecodePatch) continue val patchName = patch.patchName
this.addAll(patch.signatures)
} // if the patch has already applied silently skip it
} if (appliedPatches.contains(patchName)) return PatchResultSuccess()
if (signatures.isEmpty()) { appliedPatches.add(patchName)
return emptyList()
// recursively apply all dependency patches
patch.dependencies?.forEach {
val patchDependency = it.java
val result = applyPatch(patchDependency, appliedPatches)
if (result.isSuccess()) return@forEach
val errorMessage = result.error()!!.message
return PatchResultError("$patchName depends on ${patchDependency.patchName} but the following error was raised: $errorMessage")
} }
MethodSignatureResolver(patcherData.bytecodeData.classes.internalClasses, signatures).resolve(patcherData) val patchInstance = patch.getDeclaredConstructor().newInstance()
signaturesResolved = true
return signatures // if the current patch is a resource patch but resource patching is disabled, return an error
val isResourcePatch = patchInstance is ResourcePatch
if (!options.patchResources && isResourcePatch) return PatchResultError("$patchName is a resource patch, but resource patching is disabled.")
// TODO: find a solution for this
val data = if (isResourcePatch) {
patcherData.resourceData
} else {
MethodSignatureResolver(
patcherData.bytecodeData.classes.internalClasses, (patchInstance as BytecodePatch).signatures
).resolve(patcherData)
patcherData.bytecodeData
}
return try {
patchInstance.execute(data)
} catch (e: Exception) {
PatchResultError(e)
}
} }
/** /**
* Apply patches loaded into the patcher. * Apply patches loaded into the patcher.
* @param stopOnError If true, the patches will stop on the first error. * @param stopOnError If true, the patches will stop on the first error.
@@ -198,43 +222,22 @@ class Patcher(
fun applyPatches( fun applyPatches(
stopOnError: Boolean = false, callback: (String) -> Unit = {} stopOnError: Boolean = false, callback: (String) -> Unit = {}
): Map<String, Result<PatchResultSuccess>> { ): Map<String, Result<PatchResultSuccess>> {
if (!signaturesResolved) { val appliedPatches = mutableListOf<String>()
resolveSignatures()
}
return buildMap { return buildMap {
for (patch in patcherData.patches) { for (patch in patcherData.patches) {
val resourcePatch = patch is ResourcePatch val result = applyPatch(patch, appliedPatches)
if (!patchResources && resourcePatch) continue
val patchNameAnnotation = patch::class.java.findAnnotationRecursively(Name::class.java) val name = patch.patchName
callback(name)
patchNameAnnotation?.let { this[name] = if (result.isSuccess()) {
callback(it.name) Result.success(result.success()!!)
} else {
Result.failure(result.error()!!)
} }
val result: Result<PatchResultSuccess> = try { if (stopOnError && result.isError()) break
val data = if (resourcePatch) {
patcherData.resourceData
} else {
patcherData.bytecodeData
}
val pr = patch.execute(data)
if (pr.isSuccess()) {
Result.success(pr.success()!!)
} else {
Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error"))
}
} catch (e: Exception) {
Result.failure(e)
}
patchNameAnnotation?.let {
this[patchNameAnnotation.name] = result
}
if (result.isFailure && stopOnError) break
} }
} }
} }

View File

@@ -0,0 +1,15 @@
package app.revanced.patcher
import java.io.File
/**
* @param inputFile The input file (usually an apk file).
* @param resourceCacheDirectory Directory to cache resources.
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.
*/
data class PatcherOptions(
internal val inputFile: File,
// TODO: maybe a file system in memory is better. Could cause high memory usage.
internal val resourceCacheDirectory: String,
internal val patchResources: Boolean = false
)

View File

@@ -11,8 +11,8 @@ internal data class PatcherData(
val internalClasses: MutableList<ClassDef>, val internalClasses: MutableList<ClassDef>,
val resourceCacheDirectory: String val resourceCacheDirectory: String
) { ) {
internal val patches = mutableListOf<Patch<Data>>() internal val patches = mutableListOf<Class<out Patch<Data>>>()
internal val bytecodeData = BytecodeData(patches, internalClasses) internal val bytecodeData = BytecodeData(internalClasses)
internal val resourceData = ResourceData(File(resourceCacheDirectory)) internal val resourceData = ResourceData(File(resourceCacheDirectory))
} }

View File

@@ -1,55 +1,33 @@
package app.revanced.patcher.data.implementation package app.revanced.patcher.data.implementation
import app.revanced.patcher.data.base.Data import app.revanced.patcher.data.base.Data
import app.revanced.patcher.patch.base.Patch
import app.revanced.patcher.patch.implementation.BytecodePatch
import app.revanced.patcher.signature.implementation.method.resolver.SignatureResolverResult
import app.revanced.patcher.util.ProxyBackedClassList import app.revanced.patcher.util.ProxyBackedClassList
import app.revanced.patcher.util.method.MethodWalker import app.revanced.patcher.util.method.MethodWalker
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method import org.jf.dexlib2.iface.Method
class BytecodeData( class BytecodeData(
// FIXME: ugly solution due to design.
// It does not make sense for a BytecodeData instance to have access to the patches
private val patches: List<Patch<Data>>,
internalClasses: MutableList<ClassDef> internalClasses: MutableList<ClassDef>
) : Data { ) : Data {
val classes = ProxyBackedClassList(internalClasses) val classes = ProxyBackedClassList(internalClasses)
/** /**
* Find a class by a given class name * Find a class by a given class name.
* @return A proxy for the first class that matches the class name * @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/ */
fun findClass(className: String) = findClass { it.type.contains(className) } fun findClass(className: String) = findClass { it.type.contains(className) }
/** /**
* Find a class by a given predicate * Find a class by a given predicate.
* @return A proxy for the first class that matches the predicate * @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/ */
fun findClass(predicate: (ClassDef) -> Boolean): app.revanced.patcher.util.proxy.ClassProxy? { fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate... // if we already proxied the class matching the predicate...
for (patch in patches) { classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
if (patch !is BytecodePatch) continue
for (signature in patch.signatures) {
val result = signature.result
result ?: continue
if (predicate(result.definingClassProxy.immutableClass)) return result.definingClassProxy // ...then return that proxy
}
}
// else resolve the class to a proxy and return it, if the predicate is matching a class // else resolve the class to a proxy and return it, if the predicate is matching a class
return classes.find(predicate)?.let { classes.find(predicate)?.let { proxy(it) }
proxy(it)
}
}
}
class MethodMap : LinkedHashMap<String, SignatureResolverResult>() {
override fun get(key: String): SignatureResolverResult {
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
}
} }
internal class MethodNotFoundException(s: String) : Exception(s) internal class MethodNotFoundException(s: String) : Exception(s)
@@ -63,6 +41,11 @@ internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T?
return null return null
} }
/**
* Create a [MethodWalker] instance for the current [BytecodeData].
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun BytecodeData.toMethodWalker(startMethod: Method): MethodWalker { fun BytecodeData.toMethodWalker(startMethod: Method): MethodWalker {
return MethodWalker(this, startMethod) return MethodWalker(this, startMethod)
} }
@@ -80,7 +63,7 @@ fun BytecodeData.proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.Clas
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type } var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) { if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef) proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.proxies.add(proxy) this.classes.add(proxy)
} }
return proxy return proxy
} }

View File

@@ -0,0 +1,57 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.annotation.Compatibility
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.base.Data
import app.revanced.patcher.patch.base.Patch
import app.revanced.patcher.signature.implementation.method.MethodSignature
import app.revanced.patcher.signature.implementation.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.signature.implementation.method.annotation.MatchingMethod
import kotlin.reflect.KClass
/**
* Recursively find a given annotation on a class.
* @param targetAnnotation The annotation to find.
* @return The annotation.
*/
private fun <T : Annotation> Class<*>.recursiveAnnotation(targetAnnotation: KClass<T>) =
this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf())
private fun <T : Annotation> Class<*>.findAnnotationRecursively(
targetAnnotation: Class<T>, traversed: MutableSet<Annotation>
): T? {
val found = this.annotations.firstOrNull { it.annotationClass.java.name == targetAnnotation.name }
@Suppress("UNCHECKED_CAST") if (found != null) return found as T
for (annotation in this.annotations) {
if (traversed.contains(annotation)) continue
traversed.add(annotation)
return (annotation.annotationClass.java.findAnnotationRecursively(targetAnnotation, traversed)) ?: continue
}
return null
}
object PatchExtensions {
val Class<out Patch<Data>>.patchName: String
get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version
val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description
val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Patch::class)?.dependencies
val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages
}
object MethodSignatureExtensions {
val MethodSignature.name get() = javaClass.recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
val MethodSignature.version get() = javaClass.recursiveAnnotation(Version::class)?.version ?: "0.0.1"
val MethodSignature.description get() = javaClass.recursiveAnnotation(Description::class)?.description
val MethodSignature.compatiblePackages get() = javaClass.recursiveAnnotation(Compatibility::class)?.compatiblePackages
val MethodSignature.matchingMethod get() = javaClass.recursiveAnnotation(MatchingMethod::class)
val MethodSignature.fuzzyPatternScanMethod get() = javaClass.recursiveAnnotation(FuzzyPatternScanMethod::class)
val MethodSignature.fuzzyThreshold get() = fuzzyPatternScanMethod?.threshold ?: 0
}

View File

@@ -9,33 +9,7 @@ import org.jf.dexlib2.iface.reference.MethodReference
import org.jf.dexlib2.immutable.ImmutableMethod import org.jf.dexlib2.immutable.ImmutableMethod
import org.jf.dexlib2.immutable.ImmutableMethodImplementation import org.jf.dexlib2.immutable.ImmutableMethodImplementation
import org.jf.dexlib2.util.MethodUtil import org.jf.dexlib2.util.MethodUtil
import java.io.OutputStream
/**
* Recursively find a given annotation on a class
* @param targetAnnotation The annotation to find
* @return The annotation
*/
fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: Class<T>) =
this.findAnnotationRecursively(targetAnnotation, mutableSetOf())
private fun <T : Annotation> Class<*>.findAnnotationRecursively(
targetAnnotation: Class<T>,
traversed: MutableSet<Annotation>
): T? {
val found = this.annotations.firstOrNull { it.annotationClass.java.name == targetAnnotation.name }
@Suppress("UNCHECKED_CAST")
if (found != null) return found as T
for (annotation in this.annotations) {
if (traversed.contains(annotation)) continue
traversed.add(annotation)
return (annotation.annotationClass.java.findAnnotationRecursively(targetAnnotation, traversed)) ?: continue
}
return null
}
infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value
infix fun Int.or(other: AccessFlags) = this or other.value infix fun Int.or(other: AccessFlags) = this or other.value
@@ -46,6 +20,19 @@ fun MutableMethodImplementation.addInstructions(index: Int, instructions: List<B
} }
} }
/**
* Compare a method to another, considering constructors and parameters.
* @param otherMethod The method to compare against.
* @return True if the methods match given the conditions.
*/
fun Method.softCompareTo(
otherMethod: MethodReference
): Boolean {
if (MethodUtil.isConstructor(this) && !parametersEqual(this.parameterTypes, otherMethod.parameterTypes))
return false
return this.name == otherMethod.name
}
/** /**
* Clones the method. * Clones the method.
* @param registerCount This parameter allows you to change the register count of the method. * @param registerCount This parameter allows you to change the register count of the method.
@@ -85,14 +72,6 @@ internal fun Method.cloneMutable(
registerCount: Int = 0, registerCount: Int = 0,
) = clone(registerCount).toMutable() ) = clone(registerCount).toMutable()
internal fun Method.softCompareTo(
otherMethod: MethodReference
): Boolean {
if (MethodUtil.isConstructor(this) && !parametersEqual(this.parameterTypes, otherMethod.parameterTypes))
return false
return this.name == otherMethod.name
}
// FIXME: also check the order of parameters as different order equals different method overload // FIXME: also check the order of parameters as different order equals different method overload
internal fun parametersEqual( internal fun parametersEqual(
parameters1: Iterable<CharSequence>, parameters1: Iterable<CharSequence>,
@@ -105,4 +84,9 @@ internal fun parametersEqual(
) )
} }
} }
} }
internal val nullOutputStream: OutputStream =
object : OutputStream() {
override fun write(b: Int) {}
}

View File

@@ -1,9 +1,14 @@
package app.revanced.patcher.patch.annotations package app.revanced.patcher.patch.annotations
import app.revanced.patcher.data.base.Data
import kotlin.reflect.KClass
/** /**
* Annotation to mark a Class as a patch. * Annotation to mark a Class as a patch.
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented @MustBeDocumented
annotation class Patch annotation class Patch(
val dependencies: Array<KClass<out app.revanced.patcher.patch.base.Patch<Data>>> = []
)

View File

@@ -11,8 +11,9 @@ import app.revanced.patcher.patch.implementation.misc.PatchResult
* Can either be a [ResourcePatch] or a [BytecodePatch]. * Can either be a [ResourcePatch] or a [BytecodePatch].
*/ */
abstract class Patch<out T : Data> { abstract class Patch<out T : Data> {
/** /**
* The main function of the [Patch] which the patcher will call. * The main function of the [Patch] which the patcher will call.
*/ */
abstract fun execute(data: @UnsafeVariance T): PatchResult // FIXME: remove the UnsafeVariance annotation abstract fun execute(data: @UnsafeVariance T): PatchResult
} }

View File

@@ -24,10 +24,12 @@ interface PatchResult {
} }
} }
class PatchResultError(private val errorMessage: String) : PatchResult { class PatchResultError(
fun errorMessage(): String { errorMessage: String?, cause: Exception?
return errorMessage ) : Exception(errorMessage, cause), PatchResult {
} constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Exception) : this(cause.message, cause)
} }
class PatchResultSuccess : PatchResult class PatchResultSuccess : PatchResult

View File

@@ -1,7 +1,7 @@
package app.revanced.patcher.signature.implementation.method package app.revanced.patcher.signature.implementation.method
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.data.implementation.MethodNotFoundException import app.revanced.patcher.data.implementation.MethodNotFoundException
import app.revanced.patcher.extensions.MethodSignatureExtensions.name
import app.revanced.patcher.signature.base.Signature import app.revanced.patcher.signature.base.Signature
import app.revanced.patcher.signature.implementation.method.resolver.SignatureResolverResult import app.revanced.patcher.signature.implementation.method.resolver.SignatureResolverResult
import org.jf.dexlib2.Opcode import org.jf.dexlib2.Opcode
@@ -26,22 +26,8 @@ abstract class MethodSignature(
* The result of the signature * The result of the signature
*/ */
var result: SignatureResolverResult? = null var result: SignatureResolverResult? = null
@Throws(MethodNotFoundException::class)
get() { get() {
return field ?: throw MethodNotFoundException( return field ?: throw MethodNotFoundException("Could not resolve required signature ${this.name}")
"Could not resolve required signature ${
(this::class.annotations.find { it is Name }?.let {
(it as Name).name
})
}"
)
}
val resolved: Boolean
get() {
var resolved = false
try {
resolved = result != null
} catch (_: Exception) {
}
return resolved
} }
} }

View File

@@ -10,7 +10,7 @@ import app.revanced.patcher.signature.implementation.method.MethodSignature
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class MatchingMethod( annotation class MatchingMethod(
val definingClass: String = "L<unspecified-class>", val definingClass: String = "L<unspecified-class>;",
val name: String = "<unspecified-method>" val name: String = "<unspecified-method>"
) )

View File

@@ -2,10 +2,9 @@ package app.revanced.patcher.signature.implementation.method.resolver
import app.revanced.patcher.data.PatcherData import app.revanced.patcher.data.PatcherData
import app.revanced.patcher.data.implementation.proxy import app.revanced.patcher.data.implementation.proxy
import app.revanced.patcher.extensions.findAnnotationRecursively import app.revanced.patcher.extensions.MethodSignatureExtensions.fuzzyThreshold
import app.revanced.patcher.extensions.parametersEqual import app.revanced.patcher.extensions.parametersEqual
import app.revanced.patcher.signature.implementation.method.MethodSignature import app.revanced.patcher.signature.implementation.method.MethodSignature
import app.revanced.patcher.signature.implementation.method.annotation.FuzzyPatternScanMethod
import org.jf.dexlib2.Opcode import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method import org.jf.dexlib2.iface.Method
@@ -109,9 +108,7 @@ internal class MethodSignatureResolver(
val pattern = signature.opcodes!! val pattern = signature.opcodes!!
val size = pattern.count() val size = pattern.count()
val threshold = val threshold = signature.fuzzyThreshold
signature::class.java.findAnnotationRecursively(FuzzyPatternScanMethod::class.java)?.threshold
?: 0
for (instructionIndex in 0 until count) { for (instructionIndex in 0 until count) {
var patternIndex = 0 var patternIndex = 0

View File

@@ -1,24 +1,21 @@
package app.revanced.patcher.util package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
class ProxyBackedClassList(internal val internalClasses: MutableList<ClassDef>) : List<ClassDef> { class ProxyBackedClassList(internal val internalClasses: MutableList<ClassDef>) : List<ClassDef> {
internal val proxies = mutableListOf<app.revanced.patcher.util.proxy.ClassProxy>() private val internalProxies = mutableListOf<ClassProxy>()
internal val proxies: List<ClassProxy> = internalProxies
fun add(classDef: ClassDef) { fun add(classDef: ClassDef) = internalClasses.add(classDef)
internalClasses.add(classDef) fun add(classProxy: ClassProxy) = internalProxies.add(classProxy)
}
fun add(classProxy: app.revanced.patcher.util.proxy.ClassProxy) {
proxies.add(classProxy)
}
/** /**
* Apply all resolved classes into [internalClasses] and clean the [proxies] list. * Apply all resolved classes into [internalClasses] and clean the [proxies] list.
*/ */
fun applyProxies() { internal fun applyProxies() {
// FIXME: check if this could cause issues when multiple patches use the same proxy // FIXME: check if this could cause issues when multiple patches use the same proxy
proxies.removeIf { proxy -> internalProxies.removeIf { proxy ->
// if the proxy is unused, keep it in the list // if the proxy is unused, keep it in the list
if (!proxy.proxyUsed) return@removeIf false if (!proxy.proxyUsed) return@removeIf false

View File

@@ -33,7 +33,7 @@ class MethodWalker internal constructor(
* @param walkMutable If this is true, the class of the method will be resolved mutably. * @param walkMutable If this is true, the class of the method will be resolved mutably.
* The current method will be mutable. * The current method will be mutable.
*/ */
fun walk(offset: Int, walkMutable: Boolean = false): MethodWalker { fun nextMethod(offset: Int, walkMutable: Boolean = false): MethodWalker {
currentMethod.implementation?.instructions?.let { instructions -> currentMethod.implementation?.instructions?.let { instructions ->
val instruction = instructions.elementAt(offset) val instruction = instructions.elementAt(offset)

View File

@@ -1,34 +0,0 @@
package app.revanced.patcher.util.patch
import app.revanced.patcher.patch.base.Patch
import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
object PatchLoader {
/**
* This method loads patches from a given jar file containing [Patch]es
* @return the loaded patches represented as a list of [Patch] classes
*/
fun loadFromFile(patchesJar: File) = buildList {
val jarFile = JarFile(patchesJar)
val classLoader = URLClassLoader(arrayOf(patchesJar.toURI().toURL()))
val entries = jarFile.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (!entry.name.endsWith(".class") || entry.name.contains("$")) continue
val clazz = classLoader.loadClass(entry.realName.replace('/', '.').replace(".class", ""))
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) continue
@Suppress("UNCHECKED_CAST")
val patch = clazz as Class<Patch<*>>
// TODO: include declared classes from patch
this.add(patch)
}
}
}

View File

@@ -0,0 +1,18 @@
package app.revanced.patcher.util.patch.base
import app.revanced.patcher.data.base.Data
import app.revanced.patcher.patch.base.Patch
import java.io.File
/**
* @param patchBundlePath The path to the patch bundle.
*/
abstract class PatchBundle(patchBundlePath: String) : File(patchBundlePath) {
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
classNames.forEach { className ->
val clazz = classLoader.loadClass(className)
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) return@forEach
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Data>>)
}
}
}

View File

@@ -0,0 +1,17 @@
package app.revanced.patcher.util.patch.implementation
import app.revanced.patcher.util.patch.base.PatchBundle
import app.revanced.patcher.util.patch.util.StringIterator
import org.jf.dexlib2.DexFileFactory
/**
* A patch bundle of the ReVanced [DexPatchBundle] format.
* @param patchBundlePath The path to a patch bundle of dex format.
* @param dexClassLoader The dex class loader.
*/
class DexPatchBundle(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) {
fun loadPatches() = loadPatches(dexClassLoader,
StringIterator(DexFileFactory.loadDexFile(path, null).classes.iterator()) { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
})
}

View File

@@ -0,0 +1,30 @@
package app.revanced.patcher.util.patch.implementation
import app.revanced.patcher.util.patch.base.PatchBundle
import app.revanced.patcher.util.patch.util.StringIterator
import java.net.URLClassLoader
import java.util.jar.JarFile
/**
* A patch bundle of the ReVanced [JarPatchBundle] format.
* @param patchBundlePath The path to the patch bundle.
*/
class JarPatchBundle(patchBundlePath: String) : PatchBundle(patchBundlePath) {
fun loadPatches() = loadPatches(
URLClassLoader(
arrayOf(this.toURI().toURL()),
Thread.currentThread().contextClassLoader // TODO: find out why this is required
),
StringIterator(
JarFile(this)
.entries()
.toList() // TODO: find a cleaner solution than that to filter non class files
.filter {
it.name.endsWith(".class") && !it.name.contains("$")
}
.iterator()
) {
it.realName.replace('/', '.').replace(".class", "")
}
)
}

View File

@@ -0,0 +1,10 @@
package app.revanced.patcher.util.patch.util
internal class StringIterator<T, I : Iterator<T>>(
private val iterator: I,
private val _next: (T) -> String
) : Iterator<String> {
override fun hasNext() = iterator.hasNext()
override fun next() = _next(iterator.next())
}