Skip to content

Commit

Permalink
feat: patch dependencies annotation and PatcherOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
oSumAtrIX committed Jun 5, 2022
1 parent 26f3e73 commit 6c65952
Show file tree
Hide file tree
Showing 15 changed files with 180 additions and 221 deletions.
133 changes: 68 additions & 65 deletions src/main/kotlin/app/revanced/patcher/Patcher.kt
@@ -1,16 +1,17 @@
package app.revanced.patcher

import app.revanced.patcher.annotation.Name
import app.revanced.patcher.data.PatcherData
import app.revanced.patcher.data.base.Data
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.implementation.BytecodePatch
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.signature.implementation.method.MethodSignature
import app.revanced.patcher.signature.implementation.method.resolver.MethodSignatureResolver
import app.revanced.patcher.util.ListBackedSet
import brut.androlib.Androlib
Expand All @@ -34,27 +35,21 @@ val NAMER = BasicDexFileNamer()

/**
* The ReVanced Patcher.
* @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.
* @param options The options for the patcher.
*/
class Patcher(
inputFile: File,
// TODO: maybe a file system in memory is better. Could cause high memory usage.
private val resourceCacheDirectory: String,
private val patchResources: Boolean = false
private val options: PatcherOptions
) {
val packageVersion: String
val packageName: String

private lateinit var usesFramework: UsesFramework
private val patcherData: PatcherData
private val opcodes: Opcodes
private var signaturesResolved = false

init {
val extFileInput = ExtFile(inputFile)
val outDir = File(resourceCacheDirectory)
val extFileInput = ExtFile(options.inputFile)
val outDir = File(options.resourceCacheDirectory)

if (outDir.exists()) outDir.deleteRecursively()
outDir.mkdir()
Expand All @@ -63,7 +58,7 @@ class Patcher(
val androlib = Androlib()
val resourceTable = androlib.getResTable(extFileInput, true)

if (patchResources) {
if (options.patchResources) {
// 1. decode resources to cache directory
androlib.decodeManifestWithResources(extFileInput, outDir, resourceTable)
androlib.decodeResourcesFull(extFileInput, outDir, resourceTable)
Expand Down Expand Up @@ -93,11 +88,11 @@ class Patcher(
packageVersion = resourceTable.versionInfo.versionName
packageName = resourceTable.currentResPackage.name
// 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

// save to patcher data
patcherData = PatcherData(dexFile.classes.toMutableList(), resourceCacheDirectory)
patcherData = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory)
}

/**
Expand Down Expand Up @@ -144,8 +139,8 @@ class Patcher(
}

// build modified resources
if (patchResources) {
val extDir = ExtFile(resourceCacheDirectory)
if (options.patchResources) {
val extDir = ExtFile(options.resourceCacheDirectory)

// TODO: figure out why a new instance of Androlib is necessary here
Androlib().buildResources(extDir, usesFramework)
Expand All @@ -161,33 +156,62 @@ class Patcher(
}

/**
* Add a patch to the patcher.
* @param patches The patches to add.
* Add [Patch]es to the patcher.
* @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)
}

/**
* 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> {
val signatures = buildList {
for (patch in patcherData.patches) {
if (patch !is BytecodePatch) continue
this.addAll(patch.signatures)
}
private fun applyPatch(
patch: Class<out Patch<Data>>, appliedPatches: MutableList<String>
): PatchResult {
val patchName = patch.patchName

// if the patch has already applied silently skip it
if (appliedPatches.contains(patchName)) return PatchResultSuccess()
appliedPatches.add(patchName)

// 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")
}
if (signatures.isEmpty()) {
return emptyList()

val patchInstance = patch.getDeclaredConstructor().newInstance()

// 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
}

MethodSignatureResolver(patcherData.bytecodeData.classes.internalClasses, signatures).resolve(patcherData)
signaturesResolved = true
return signatures
return try {
patchInstance.execute(data)
} catch (e: Exception) {
PatchResultError(e)
}
}


/**
* Apply patches loaded into the patcher.
* @param stopOnError If true, the patches will stop on the first error.
Expand All @@ -198,43 +222,22 @@ class Patcher(
fun applyPatches(
stopOnError: Boolean = false, callback: (String) -> Unit = {}
): Map<String, Result<PatchResultSuccess>> {
if (!signaturesResolved) {
resolveSignatures()
}
val appliedPatches = mutableListOf<String>()

return buildMap {
for (patch in patcherData.patches) {
val resourcePatch = patch is ResourcePatch
if (!patchResources && resourcePatch) continue

val patchNameAnnotation = patch::class.java.findAnnotationRecursively(Name::class.java)

patchNameAnnotation?.let {
callback(it.name)
}
val result = applyPatch(patch, appliedPatches)

val result: Result<PatchResultSuccess> = try {
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)
}
val name = patch.patchName
callback(name)

patchNameAnnotation?.let {
this[patchNameAnnotation.name] = result
this[name] = if (result.isSuccess()) {
Result.success(result.success()!!)
} else {
Result.failure(result.error()!!)
}

if (result.isFailure && stopOnError) break
if (stopOnError && result.isError()) break
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/main/kotlin/app/revanced/patcher/PatcherOptions.kt
@@ -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
)
4 changes: 2 additions & 2 deletions src/main/kotlin/app/revanced/patcher/data/PatcherData.kt
Expand Up @@ -11,8 +11,8 @@ internal data class PatcherData(
val internalClasses: MutableList<ClassDef>,
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))
}
@@ -1,55 +1,33 @@
package app.revanced.patcher.data.implementation

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.method.MethodWalker
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method

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>
) : Data {
val classes = ProxyBackedClassList(internalClasses)

/**
* Find a class by a given class name
* @return A proxy for the first class that matches the class name
* Find a class by a given 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) }

/**
* Find a class by a given predicate
* @return A proxy for the first class that matches the predicate
* Find a class by a given 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...
for (patch in patches) {
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
}
}
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
return classes.find(predicate)?.let {
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")
}
classes.find(predicate)?.let { proxy(it) }
}

internal class MethodNotFoundException(s: String) : Exception(s)
Expand All @@ -63,6 +41,11 @@ internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T?
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 {
return MethodWalker(this, startMethod)
}
Expand All @@ -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 }
if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.proxies.add(proxy)
this.classes.add(proxy)
}
return proxy
}

0 comments on commit 6c65952

Please sign in to comment.