refactor: improve structuring of classes and their implementations

BREAKING CHANGE: various changes in which packages classes previously where and their implementation
This commit is contained in:
oSumAtrIX
2022-10-05 03:35:58 +02:00
parent d37452997b
commit 4aa14bbb85
20 changed files with 438 additions and 407 deletions

View File

@ -1,19 +1,13 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.data.impl.findIndexed import app.revanced.patcher.data.findIndexed
import app.revanced.patcher.extensions.PatchExtensions.dependencies import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.deprecated import app.revanced.patcher.extensions.PatchExtensions.deprecated
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.nullOutputStream import app.revanced.patcher.extensions.nullOutputStream
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultError
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.util.ListBackedSet
import app.revanced.patcher.util.VersionReader import app.revanced.patcher.util.VersionReader
import brut.androlib.Androlib import brut.androlib.Androlib
import brut.androlib.meta.UsesFramework import brut.androlib.meta.UsesFramework
@ -29,10 +23,8 @@ import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO import lanchon.multidexlib2.MultiDexIO
import org.jf.dexlib2.Opcodes import org.jf.dexlib2.Opcodes
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.Closeable
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
@ -46,7 +38,7 @@ class Patcher(private val options: PatcherOptions) {
private val logger = options.logger private val logger = options.logger
private val opcodes: Opcodes private val opcodes: Opcodes
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
val data: PatcherData val context: PatcherContext
companion object { companion object {
@JvmStatic @JvmStatic
@ -64,8 +56,8 @@ class Patcher(private val options: PatcherOptions) {
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null) val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
// get the opcodes // get the opcodes
opcodes = dexFile.opcodes opcodes = dexFile.opcodes
// finally create patcher data // finally create patcher context
data = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory) context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
// decode manifest file // decode manifest file
decodeResources(ResourceDecodingMode.MANIFEST_ONLY) decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
@ -88,12 +80,12 @@ class Patcher(private val options: PatcherOptions) {
for (classDef in MultiDexIO.readDexFile(true, file, NAMER, null, null).classes) { for (classDef in MultiDexIO.readDexFile(true, file, NAMER, null, null).classes) {
val type = classDef.type val type = classDef.type
val existingClass = data.bytecodeData.classes.internalClasses.findIndexed { it.type == type } val existingClass = context.bytecodeContext.classes.classes.findIndexed { it.type == type }
if (existingClass == null) { if (existingClass == null) {
if (throwOnDuplicates) throw Exception("Class $type has already been added to the patcher") if (throwOnDuplicates) throw Exception("Class $type has already been added to the patcher")
logger.trace("Merging $type") logger.trace("Merging $type")
data.bytecodeData.classes.internalClasses.add(classDef) context.bytecodeContext.classes.classes.add(classDef)
modified = true modified = true
continue continue
@ -104,7 +96,7 @@ class Patcher(private val options: PatcherOptions) {
logger.trace("Overwriting $type") logger.trace("Overwriting $type")
val index = existingClass.second val index = existingClass.second
data.bytecodeData.classes.internalClasses[index] = classDef context.bytecodeContext.classes.classes[index] = classDef
modified = true modified = true
} }
if (modified) callback(file) if (modified) callback(file)
@ -115,7 +107,7 @@ class Patcher(private val options: PatcherOptions) {
* Save the patched dex file. * Save the patched dex file.
*/ */
fun save(): PatcherResult { fun save(): PatcherResult {
val packageMetadata = data.packageMetadata val packageMetadata = context.packageMetadata
val metaInfo = packageMetadata.metaInfo val metaInfo = packageMetadata.metaInfo
var resourceFile: File? = null var resourceFile: File? = null
@ -168,14 +160,8 @@ class Patcher(private val options: PatcherOptions) {
logger.trace("Creating new dex file") logger.trace("Creating new dex file")
val newDexFile = object : DexFile { val newDexFile = object : DexFile {
override fun getClasses(): Set<ClassDef> { override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
data.bytecodeData.classes.applyProxies() override fun getOpcodes() = this@Patcher.opcodes
return ListBackedSet(data.bytecodeData.classes.internalClasses)
}
override fun getOpcodes(): Opcodes {
return this@Patcher.opcodes
}
} }
// write modified dex files // write modified dex files
@ -199,12 +185,12 @@ class Patcher(private val options: PatcherOptions) {
* Add [Patch]es to the patcher. * Add [Patch]es to the patcher.
* @param patches [Patch]es The patches to add. * @param patches [Patch]es The patches to add.
*/ */
fun addPatches(patches: Iterable<Class<out Patch<Data>>>) { fun addPatches(patches: Iterable<Class<out Patch<Context>>>) {
/** /**
* Fill the cache with the instances of the [Patch]es for later use. * Fill the cache with the instances of the [Patch]es for later use.
* Note: Dependencies of the [Patch] will be cached as well. * Note: Dependencies of the [Patch] will be cached as well.
*/ */
fun Class<out Patch<Data>>.isResource() { fun Class<out Patch<Context>>.isResource() {
this.also { this.also {
if (!ResourcePatch::class.java.isAssignableFrom(it)) return@also if (!ResourcePatch::class.java.isAssignableFrom(it)) return@also
// set the mode to decode all resources before running the patches // set the mode to decode all resources before running the patches
@ -212,74 +198,7 @@ class Patcher(private val options: PatcherOptions) {
}.dependencies?.forEach { it.java.isResource() } }.dependencies?.forEach { it.java.isResource() }
} }
data.patches.addAll(patches.onEach(Class<out Patch<Data>>::isResource)) context.patches.addAll(patches.onEach(Class<out Patch<Context>>::isResource))
}
/**
* Apply a [patch] and its dependencies recursively.
* @param patch The [patch] to apply.
* @param appliedPatches A map of [patch]es paired to a boolean indicating their success, to prevent infinite recursion.
* @return The result of executing the [patch].
*/
private fun applyPatch(
patch: Class<out Patch<Data>>,
appliedPatches: LinkedHashMap<String, AppliedPatch>
): PatchResult {
val patchName = patch.patchName
// if the patch has already applied silently skip it
if (appliedPatches.contains(patchName)) {
if (!appliedPatches[patchName]!!.success)
return PatchResultError("'$patchName' did not succeed previously")
logger.trace("Skipping '$patchName' because it has already been applied")
return PatchResultSuccess()
}
// recursively apply all dependency patches
patch.dependencies?.forEach { dependencyClass ->
val dependency = dependencyClass.java
val result = applyPatch(dependency, appliedPatches)
if (result.isSuccess()) return@forEach
val error = result.error()!!
val errorMessage = error.cause ?: error.message
return PatchResultError("'$patchName' depends on '${dependency.patchName}' but the following error was raised: $errorMessage")
}
patch.deprecated?.let { (reason, replacement) ->
logger.warn("'$patchName' is deprecated, reason: $reason")
if (replacement != null) logger.warn("Use '${replacement.java.patchName}' instead")
}
val patchInstance = patch.getDeclaredConstructor().newInstance()
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patch)
// TODO: implement this in a more polymorphic way
val data = if (isResourcePatch) {
data.resourceData
} else {
data.bytecodeData.also { data ->
(patchInstance as BytecodePatch).fingerprints?.resolve(
data,
data.classes.internalClasses
)
}
}
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
return try {
patchInstance.execute(data).also {
appliedPatches[patchName] = AppliedPatch(patchInstance, it.isSuccess())
}
} catch (e: Exception) {
PatchResultError(e).also {
appliedPatches[patchName] = AppliedPatch(patchInstance, false)
}
}
} }
/** /**
@ -310,7 +229,7 @@ class Patcher(private val options: PatcherOptions) {
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable) androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
// read additional metadata from the resource table // read additional metadata from the resource table
data.packageMetadata.let { metadata -> context.packageMetadata.let { metadata ->
metadata.metaInfo.usesFramework = UsesFramework().also { framework -> metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted() framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
} }
@ -344,7 +263,7 @@ class Patcher(private val options: PatcherOptions) {
} }
// read of the resourceTable which is created by reading the manifest file // read of the resourceTable which is created by reading the manifest file
data.packageMetadata.let { metadata -> context.packageMetadata.let { metadata ->
metadata.packageName = resourceTable.currentResPackage.name metadata.packageName = resourceTable.currentResPackage.name
metadata.packageVersion = resourceTable.versionInfo.versionName metadata.packageVersion = resourceTable.versionInfo.versionName
metadata.metaInfo.versionInfo = resourceTable.versionInfo metadata.metaInfo.versionInfo = resourceTable.versionInfo
@ -356,37 +275,105 @@ class Patcher(private val options: PatcherOptions) {
} }
/** /**
* Apply patches loaded into the patcher. * Execute patches added 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.
* @return A pair of the name of the [Patch] and its [PatchResult]. * @return A pair of the name of the [Patch] and its [PatchResult].
*/ */
fun applyPatches(stopOnError: Boolean = false) = sequence { fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
// prevent from decoding the manifest twice if it is not needed /**
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL) * Execute a [Patch] and its dependencies recursively.
*
* @param patchClass The [Patch] to execute.
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
* @return The result of executing the [Patch].
*/
fun executePatch(
patchClass: Class<out Patch<Context>>,
executedPatches: LinkedHashMap<String, ExecutedPatch>
): PatchResult {
val patchName = patchClass.patchName
logger.trace("Applying all patches") // if the patch has already applied silently skip it
if (executedPatches.contains(patchName)) {
if (!executedPatches[patchName]!!.success)
return PatchResultError("'$patchName' did not succeed previously")
val appliedPatches = LinkedHashMap<String, AppliedPatch>() // first is name logger.trace("Skipping '$patchName' because it has already been applied")
try { return PatchResultSuccess()
for (patch in data.patches) {
val patchResult = applyPatch(patch, appliedPatches)
val result = if (patchResult.isSuccess()) {
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) break
} }
} finally {
// close all closeable patches in order
for ((patch, _) in appliedPatches.values.reversed()) {
if (patch !is Closeable) continue
patch.close() // recursively execute all dependency patches
patchClass.dependencies?.forEach { dependencyClass ->
val dependency = dependencyClass.java
val result = executePatch(dependency, executedPatches)
if (result.isSuccess()) return@forEach
val error = result.error()!!
val errorMessage = error.cause ?: error.message
return PatchResultError("'$patchName' depends on '${dependency.patchName}' but the following error was raised: $errorMessage")
}
patchClass.deprecated?.let { (reason, replacement) ->
logger.warn("'$patchName' is deprecated, reason: $reason")
if (replacement != null) logger.warn("Use '${replacement.java.patchName}' instead")
}
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
// TODO: implement this in a more polymorphic way
val patchContext = if (isResourcePatch) {
context.resourceContext
} else {
context.bytecodeContext.also { context ->
(patchInstance as BytecodePatch).fingerprints?.resolve(
context,
context.classes.classes
)
}
}
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
return try {
patchInstance.execute(patchContext).also {
executedPatches[patchName] = ExecutedPatch(patchInstance, it.isSuccess())
}
} catch (e: Exception) {
PatchResultError(e).also {
executedPatches[patchName] = ExecutedPatch(patchInstance, false)
}
}
}
return sequence {
// prevent from decoding the manifest twice if it is not needed
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
logger.trace("Executing all patches")
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // first is name
try {
context.patches.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
val result = if (patchResult.isSuccess()) {
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) return@sequence
}
} finally {
executedPatches.values.reversed().forEach { (patch, _) ->
patch.close()
}
} }
} }
} }
@ -408,9 +395,9 @@ class Patcher(private val options: PatcherOptions) {
} }
/** /**
* A result of applying a [Patch]. * A result of executing a [Patch].
* *
* @param patchInstance The instance of the [Patch] that was applied. * @param patchInstance The instance of the [Patch] that was applied.
* @param success The result of the [Patch]. * @param success The result of the [Patch].
*/ */
internal data class AppliedPatch(val patchInstance: Patch<Data>, val success: Boolean) internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)

View File

@ -0,0 +1,19 @@
package app.revanced.patcher
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.Context
import app.revanced.patcher.data.PackageMetadata
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch
import org.jf.dexlib2.iface.ClassDef
import java.io.File
data class PatcherContext(
val classes: MutableList<ClassDef>,
val resourceCacheDirectory: File,
) {
val packageMetadata = PackageMetadata()
internal val patches = mutableListOf<Class<out Patch<Context>>>()
internal val bytecodeContext = BytecodeContext(classes)
internal val resourceContext = ResourceContext(resourceCacheDirectory)
}

View File

@ -1,19 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.data.Data
import app.revanced.patcher.data.PackageMetadata
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.data.impl.ResourceData
import app.revanced.patcher.patch.Patch
import org.jf.dexlib2.iface.ClassDef
import java.io.File
data class PatcherData(
val internalClasses: MutableList<ClassDef>,
val resourceCacheDirectory: String,
) {
val packageMetadata = PackageMetadata()
internal val patches = mutableListOf<Class<out Patch<Data>>>()
internal val bytecodeData = BytecodeData(internalClasses)
internal val resourceData = ResourceData(File(resourceCacheDirectory))
}

View File

@ -1,6 +1,6 @@
package app.revanced.patcher.annotation package app.revanced.patcher.annotation
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -14,6 +14,6 @@ import kotlin.reflect.KClass
@MustBeDocumented @MustBeDocumented
annotation class PatchDeprecated( annotation class PatchDeprecated(
val reason: String, val reason: String,
val replacement: KClass<out Patch<Data>> = Patch::class val replacement: KClass<out Patch<Context>> = Patch::class
// Values cannot be nullable in annotations, so this will have to do. // Values cannot be nullable in annotations, so this will have to do.
) )

View File

@ -1,6 +1,9 @@
package app.revanced.patcher.data.impl package app.revanced.patcher.data
import app.revanced.patcher.data.Data 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
import org.w3c.dom.Document import org.w3c.dom.Document
import java.io.Closeable import java.io.Closeable
import java.io.File import java.io.File
@ -11,7 +14,79 @@ import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult import javax.xml.transform.stream.StreamResult
class ResourceData(private val resourceCacheDirectory: File) : Data, Iterable<File> { /**
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext].
*/
sealed interface Context
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
/**
* The list of classes.
*/
val classes = ProxyBackedClassList(classes)
/**
* 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.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.add(proxy)
}
return proxy
}
private companion object {
inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
for (element in this) {
if (predicate(element)) {
return element
}
}
return null
}
}
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
return MethodWalker(this, startMethod)
}
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
for ((index, element) in this.withIndex()) {
if (predicate(element)) {
return element to index
}
}
return null
}
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
val xmlEditor = XmlFileHolder() val xmlEditor = XmlFileHolder()
operator fun get(path: String) = resourceCacheDirectory.resolve(path) operator fun get(path: String) = resourceCacheDirectory.resolve(path)
@ -23,7 +98,7 @@ class ResourceData(private val resourceCacheDirectory: File) : Data, Iterable<Fi
DomFileEditor(inputStream) DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor { operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceData[path]) return DomFileEditor(this@ResourceContext[path])
} }
} }
@ -31,7 +106,9 @@ class ResourceData(private val resourceCacheDirectory: File) : Data, Iterable<Fi
/** /**
* Wrapper for a file that can be edited as a dom document. * Wrapper for a file that can be edited as a dom document.
* Note: This constructor does not check for locks to the file when writing. Use the secondary constructor. *
* This constructor does not check for locks to the file when writing.
* Use the secondary constructor.
* *
* @param inputStream the input stream to read the xml file from. * @param inputStream the input stream to read the xml file from.
* @param outputStream the output stream to write the xml file to. If null, the file will be read only. * @param outputStream the output stream to write the xml file to. If null, the file will be read only.
@ -63,7 +140,8 @@ class DomFileEditor internal constructor(
/** /**
* Closes the editor. Write backs and decreases the lock count. * Closes the editor. Write backs and decreases the lock count.
* Note: Will not write back to the file if the file is still locked. *
* Will not write back to the file if the file is still locked.
*/ */
override fun close() { override fun close() {
if (closed) return if (closed) return
@ -100,4 +178,4 @@ class DomFileEditor internal constructor(
// map of concurrent open files // map of concurrent open files
val locks = mutableMapOf<String, Int>() val locks = mutableMapOf<String, Int>()
} }
} }

View File

@ -1,9 +0,0 @@
package app.revanced.patcher.data
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.data.impl.ResourceData
/**
* Constraint interface for [BytecodeData] and [ResourceData]
*/
interface Data

View File

@ -1,69 +0,0 @@
package app.revanced.patcher.data.impl
import app.revanced.patcher.data.Data
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(
internalClasses: MutableList<ClassDef>
) : Data {
val classes = ProxyBackedClassList(internalClasses)
/**
* 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.
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.add(proxy)
}
return proxy
}
}
internal class MethodNotFoundException(s: String) : Exception(s)
internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
for (element in this) {
if (predicate(element)) {
return element
}
}
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)
}
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
for ((index, element) in this.withIndex()) {
if (predicate(element)) {
return element to index
}
}
return null
}

View File

@ -1,11 +1,13 @@
package app.revanced.patcher.extensions package app.revanced.patcher.extensions
import app.revanced.patcher.annotation.* import app.revanced.patcher.annotation.*
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.patch.OptionsContainer import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchOptions import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.annotations.DependsOn
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KVisibility import kotlin.reflect.KVisibility
import kotlin.reflect.full.companionObject import kotlin.reflect.full.companionObject
@ -16,7 +18,7 @@ import kotlin.reflect.full.companionObjectInstance
* @param targetAnnotation The annotation to find. * @param targetAnnotation The annotation to find.
* @return The annotation. * @return The annotation.
*/ */
private fun <T : Annotation> Class<*>.recursiveAnnotation(targetAnnotation: KClass<T>) = private fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: KClass<T>) =
this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf()) this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf())
@ -38,21 +40,34 @@ private fun <T : Annotation> Class<*>.findAnnotationRecursively(
} }
object PatchExtensions { object PatchExtensions {
val Class<*>.patchName: String get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName val Class<out Patch<Context>>.patchName: String
val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version get() = findAnnotationRecursively(Name::class)?.name ?: this.javaClass.simpleName
val Class<out Patch<Data>>.include get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Patch::class)!!.include
val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description val Class<out Patch<Context>>.version
val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.DependsOn::class)?.dependencies get() = findAnnotationRecursively(Version::class)?.version
val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages
val Class<out Patch<Data>>.options: PatchOptions? val Class<out Patch<Context>>.include
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
val Class<out Patch<Context>>.description
get() = findAnnotationRecursively(Description::class)?.description
val Class<out Patch<Context>>.dependencies
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
val Class<out Patch<Context>>.compatiblePackages
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
val Class<out Patch<Context>>.options: PatchOptions?
get() = kotlin.companionObject?.let { cl -> get() = kotlin.companionObject?.let { cl ->
if (cl.visibility != KVisibility.PUBLIC) return null if (cl.visibility != KVisibility.PUBLIC) return null
kotlin.companionObjectInstance?.let { kotlin.companionObjectInstance?.let {
(it as? OptionsContainer)?.options (it as? OptionsContainer)?.options
} }
} }
val Class<out Patch<Data>>.deprecated: Pair<String, KClass<out Patch<Data>>?>?
get() = recursiveAnnotation(PatchDeprecated::class)?.let { val Class<out Patch<Context>>.deprecated: Pair<String, KClass<out Patch<Context>>?>?
get() = findAnnotationRecursively(PatchDeprecated::class)?.let {
it.reason to it.replacement.let { cl -> it.reason to it.replacement.let { cl ->
if (cl == Patch::class) null else cl if (cl == Patch::class) null else cl
} }
@ -61,9 +76,17 @@ object PatchExtensions {
object MethodFingerprintExtensions { object MethodFingerprintExtensions {
val MethodFingerprint.name: String val MethodFingerprint.name: String
get() = javaClass.recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName get() = javaClass.findAnnotationRecursively(Name::class)?.name ?: this.javaClass.simpleName
val MethodFingerprint.version get() = javaClass.recursiveAnnotation(Version::class)?.version ?: "0.0.1"
val MethodFingerprint.description get() = javaClass.recursiveAnnotation(Description::class)?.description val MethodFingerprint.version
val MethodFingerprint.fuzzyPatternScanMethod get() = javaClass.recursiveAnnotation(app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod::class) get() = javaClass.findAnnotationRecursively(Version::class)?.version ?: "0.0.1"
val MethodFingerprint.fuzzyScanThreshold get() = fuzzyPatternScanMethod?.threshold ?: 0
val MethodFingerprint.description
get() = javaClass.findAnnotationRecursively(Description::class)?.description
val MethodFingerprint.fuzzyPatternScanMethod
get() = javaClass.findAnnotationRecursively(FuzzyPatternScanMethod::class)
val MethodFingerprint.fuzzyScanThreshold
get() = fuzzyPatternScanMethod?.threshold ?: 0
} }

View File

@ -1,6 +1,6 @@
package app.revanced.patcher.fingerprint.method.impl package app.revanced.patcher.fingerprint.method.impl
import app.revanced.patcher.data.impl.BytecodeData import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyScanThreshold import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyScanThreshold
import app.revanced.patcher.extensions.parametersEqual import app.revanced.patcher.extensions.parametersEqual
@ -41,64 +41,67 @@ abstract class MethodFingerprint(
companion object { companion object {
/** /**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef]. * Resolve a list of [MethodFingerprint] against a list of [ClassDef].
* @param context The classes on which to resolve the [MethodFingerprint]. *
* @param forData The [BytecodeData] to host proxies. * @param classes The classes on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise. * @return True if the resolution was successful, false otherwise.
*/ */
fun Iterable<MethodFingerprint>.resolve(forData: BytecodeData, context: Iterable<ClassDef>) { fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) {
for (fingerprint in this) // For each fingerprint for (fingerprint in this) // For each fingerprint
classes@ for (classDef in context) // search through all classes for the fingerprint classes@ for (classDef in classes) // search through all classes for the fingerprint
if (fingerprint.resolve(forData, classDef)) if (fingerprint.resolve(context, classDef))
break@classes // if the resolution succeeded, continue with the next fingerprint break@classes // if the resolution succeeded, continue with the next fingerprint
} }
/** /**
* Resolve a [MethodFingerprint] against a [ClassDef]. * Resolve a [MethodFingerprint] against a [ClassDef].
* @param context The class on which to resolve the [MethodFingerprint]. *
* @param forData The [BytecodeData] to host proxies. * @param forClass The class on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise. * @return True if the resolution was successful, false otherwise.
*/ */
fun MethodFingerprint.resolve(forData: BytecodeData, context: ClassDef): Boolean { fun MethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean {
for (method in context.methods) for (method in forClass.methods)
if (this.resolve(forData, method, context)) if (this.resolve(context, method, forClass))
return true return true
return false return false
} }
/** /**
* Resolve a [MethodFingerprint] against a [Method]. * Resolve a [MethodFingerprint] against a [Method].
* @param context The context on which to resolve the [MethodFingerprint]. *
* @param classDef The class of the matching [Method]. * @param method The class on which to resolve the [MethodFingerprint] in.
* @param forData The [BytecodeData] to host proxies. * @param forClass The class on which to resolve the [MethodFingerprint].
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
*/ */
fun MethodFingerprint.resolve(forData: BytecodeData, context: Method, classDef: ClassDef): Boolean { fun MethodFingerprint.resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean {
val methodFingerprint = this val methodFingerprint = this
if (methodFingerprint.result != null) return true if (methodFingerprint.result != null) return true
if (methodFingerprint.returnType != null && !context.returnType.startsWith(methodFingerprint.returnType)) if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType))
return false return false
if (methodFingerprint.access != null && methodFingerprint.access != context.accessFlags) if (methodFingerprint.access != null && methodFingerprint.access != method.accessFlags)
return false return false
if (methodFingerprint.parameters != null && !parametersEqual( if (methodFingerprint.parameters != null && !parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters() methodFingerprint.parameters, // TODO: parseParameters()
context.parameterTypes method.parameterTypes
) )
) return false ) return false
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(context)) if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method))
return false return false
val stringsScanResult: StringsScanResult? = val stringsScanResult: StringsScanResult? =
if (methodFingerprint.strings != null) { if (methodFingerprint.strings != null) {
StringsScanResult( StringsScanResult(
buildList { buildList {
val implementation = context.implementation ?: return false val implementation = method.implementation ?: return false
val stringsList = methodFingerprint.strings.toMutableList() val stringsList = methodFingerprint.strings.toMutableList()
@ -124,19 +127,19 @@ abstract class MethodFingerprint(
} else null } else null
val patternScanResult = if (methodFingerprint.opcodes != null) { val patternScanResult = if (methodFingerprint.opcodes != null) {
context.implementation?.instructions ?: return false method.implementation?.instructions ?: return false
context.patternScan(methodFingerprint) ?: return false method.patternScan(methodFingerprint) ?: return false
} else null } else null
methodFingerprint.result = MethodFingerprintResult( methodFingerprint.result = MethodFingerprintResult(
context, method,
classDef, forClass,
MethodFingerprintResult.MethodFingerprintScanResult( MethodFingerprintResult.MethodFingerprintScanResult(
patternScanResult, patternScanResult,
stringsScanResult stringsScanResult
), ),
forData context
) )
return true return true
@ -215,16 +218,17 @@ private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintS
/** /**
* Represents the result of a [MethodFingerprintResult]. * Represents the result of a [MethodFingerprintResult].
*
* @param method The matching method. * @param method The matching method.
* @param classDef The [ClassDef] that contains the matching [method]. * @param classDef The [ClassDef] that contains the matching [method].
* @param scanResult The result of scanning for the [MethodFingerprint]. * @param scanResult The result of scanning for the [MethodFingerprint].
* @param data The [BytecodeData] this [MethodFingerprintResult] is attached to, to create proxies. * @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
*/ */
data class MethodFingerprintResult( data class MethodFingerprintResult(
val method: Method, val method: Method,
val classDef: ClassDef, val classDef: ClassDef,
val scanResult: MethodFingerprintScanResult, val scanResult: MethodFingerprintScanResult,
internal val data: BytecodeData internal val context: BytecodeContext
) { ) {
/** /**
@ -283,7 +287,7 @@ data class MethodFingerprintResult(
* Use [classDef] where possible. * Use [classDef] where possible.
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
val mutableClass by lazy { data.proxy(classDef).resolve() } val mutableClass by lazy { context.proxy(classDef).mutableClass }
/** /**
* Returns a mutable clone of [method] * Returns a mutable clone of [method]

View File

@ -1,34 +1,44 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.data.Data import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.impl.BytecodeData import app.revanced.patcher.data.Context
import app.revanced.patcher.data.impl.ResourceData import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import java.io.Closeable import java.io.Closeable
/** /**
* A ReVanced patch. * A ReVanced patch.
* *
* Can either be a [ResourcePatch] or a [BytecodePatch].
* If it implements [Closeable], it will be closed after all patches have been executed. * If it implements [Closeable], it will be closed after all patches have been executed.
* Closing will be done in reverse execution order. * Closing will be done in reverse execution order.
*/ */
sealed interface Patch<out T : Data> { sealed interface Patch<out T : Context> : Closeable {
/** /**
* The main function of the [Patch] which the patcher will call. * The main function of the [Patch] which the patcher will call.
*
* @param context The [Context] the patch will work on.
* @return The result of executing the patch.
*/ */
fun execute(data: @UnsafeVariance T): PatchResult fun execute(context: @UnsafeVariance T): PatchResult
/**
* The closing function for this patch.
*
* This can be treated like popping the patch from the current patch stack.
*/
override fun close() {}
} }
/** /**
* Resource patch for the Patcher. * Resource patch for the Patcher.
*/ */
interface ResourcePatch : Patch<ResourceData> interface ResourcePatch : Patch<ResourceContext>
/** /**
* Bytecode patch for the Patcher. * Bytecode patch for the Patcher.
*
* @param fingerprints A list of [MethodFingerprint] this patch relies on. * @param fingerprints A list of [MethodFingerprint] this patch relies on.
*/ */
abstract class BytecodePatch( abstract class BytecodePatch(
internal val fingerprints: Iterable<MethodFingerprint>? = null internal val fingerprints: Iterable<MethodFingerprint>? = null
) : Patch<BytecodeData> ) : Patch<BytecodeContext>

View File

@ -1,6 +1,6 @@
package app.revanced.patcher.patch.annotations package app.revanced.patcher.patch.annotations
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -20,5 +20,5 @@ annotation class Patch(val include: Boolean = true)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented @MustBeDocumented
annotation class DependsOn( annotation class DependsOn(
val dependencies: Array<KClass<out Patch<Data>>> = [] val dependencies: Array<KClass<out Patch<Context>>> = []
) )

View File

@ -3,40 +3,44 @@ package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy 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> { /**
private val internalProxies = mutableListOf<ClassProxy>() * A class that represents a set of classes and proxies.
internal val proxies: List<ClassProxy> = internalProxies *
* @param classes The classes to be backed by proxies.
fun add(classDef: ClassDef) = internalClasses.add(classDef) */
fun add(classProxy: ClassProxy) = internalProxies.add(classProxy) class ProxyBackedClassList(internal val classes: MutableList<ClassDef>) : Set<ClassDef> {
internal val proxies = mutableListOf<ClassProxy>()
/** /**
* Apply all resolved classes into [internalClasses] and clean the [proxies] list. * Add a [ClassDef].
*/ */
internal fun applyProxies() { fun add(classDef: ClassDef) = classes.add(classDef)
// FIXME: check if this could cause issues when multiple patches use the same proxy
internalProxies.removeIf { proxy ->
// if the proxy is unused, keep it in the list
if (!proxy.proxyUsed) return@removeIf false
// if it has been used, replace the internal class which it proxied /**
val index = internalClasses.indexOfFirst { it.type == proxy.immutableClass.type } * Add a [ClassProxy].
internalClasses[index] = proxy.mutatedClass */
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
/**
* Replace all classes with their mutated versions.
*/
internal fun replaceClasses() =
proxies.removeIf { proxy ->
// if the proxy is unused, keep it in the list
if (!proxy.resolved) return@removeIf false
// if it has been used, replace the original class with the new class
val index = classes.indexOfFirst { it.type == proxy.immutableClass.type }
classes[index] = proxy.mutableClass
// return true to remove it from the proxies list // return true to remove it from the proxies list
return@removeIf true return@removeIf true
} }
}
override val size get() = internalClasses.size
override fun contains(element: ClassDef) = internalClasses.contains(element) override val size get() = classes.size
override fun containsAll(elements: Collection<ClassDef>) = internalClasses.containsAll(elements) override fun contains(element: ClassDef) = classes.contains(element)
override fun get(index: Int) = internalClasses[index] override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
override fun indexOf(element: ClassDef) = internalClasses.indexOf(element) override fun isEmpty() = classes.isEmpty()
override fun isEmpty() = internalClasses.isEmpty() override fun iterator() = classes.iterator()
override fun iterator() = internalClasses.iterator()
override fun lastIndexOf(element: ClassDef) = internalClasses.lastIndexOf(element)
override fun listIterator() = internalClasses.listIterator()
override fun listIterator(index: Int) = internalClasses.listIterator(index)
override fun subList(fromIndex: Int, toIndex: Int) = internalClasses.subList(fromIndex, toIndex)
} }

View File

@ -1,7 +1,6 @@
package app.revanced.patcher.util.method package app.revanced.patcher.util.method
import app.revanced.patcher.data.impl.BytecodeData import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.impl.MethodNotFoundException
import app.revanced.patcher.extensions.softCompareTo import app.revanced.patcher.extensions.softCompareTo
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import org.jf.dexlib2.iface.Method import org.jf.dexlib2.iface.Method
@ -10,16 +9,19 @@ import org.jf.dexlib2.iface.reference.MethodReference
/** /**
* Find a method from another method via instruction offsets. * Find a method from another method via instruction offsets.
* @param bytecodeData The bytecodeData to use when resolving the next method reference. * @param bytecodeContext The context to use when resolving the next method reference.
* @param currentMethod The method to start from. * @param currentMethod The method to start from.
*/ */
class MethodWalker internal constructor( class MethodWalker internal constructor(
private val bytecodeData: BytecodeData, private val bytecodeContext: BytecodeContext,
private var currentMethod: Method private var currentMethod: Method
) { ) {
/** /**
* Get the method which was walked last. * Get the method which was walked last.
*
* It is possible to cast this method to a [MutableMethod], if the method has been walked mutably. * It is possible to cast this method to a [MutableMethod], if the method has been walked mutably.
*
* @return The method which was walked last.
*/ */
fun getMethod(): Method { fun getMethod(): Method {
return currentMethod return currentMethod
@ -27,18 +29,21 @@ class MethodWalker internal constructor(
/** /**
* Walk to a method defined at the offset in the instruction list of the current method. * Walk to a method defined at the offset in the instruction list of the current method.
*
* The current method will be mutable.
*
* @param offset The offset of the instruction. This instruction must be of format 35c. * @param offset The offset of the instruction. This instruction must be of format 35c.
* @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. * @return The same [MethodWalker] instance with the method at [offset].
*/ */
fun nextMethod(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)
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
val proxy = bytecodeData.findClass(newMethod.definingClass)!! val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
val methods = if (walkMutable) proxy.resolve().methods else proxy.immutableClass.methods val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
currentMethod = methods.first { it -> currentMethod = methods.first { it ->
return@first it.softCompareTo(newMethod) return@first it.softCompareTo(newMethod)
} }
@ -47,5 +52,5 @@ class MethodWalker internal constructor(
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}") throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")
} }
internal class MethodNotFoundException(exception: String) : Exception(exception)
} }

View File

@ -1,18 +1,74 @@
@file:Suppress("unused")
package app.revanced.patcher.util.patch package app.revanced.patcher.util.patch
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import org.jf.dexlib2.DexFileFactory
import java.io.File import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
/** /**
* A patch bundle.
* @param path The path to the patch bundle. * @param path The path to the patch bundle.
*/ */
abstract class PatchBundle(path: String) : File(path) { sealed class PatchBundle(path: String) : File(path) {
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList { internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
for (className in classNames) { for (className in classNames) {
val clazz = classLoader.loadClass(className) val clazz = classLoader.loadClass(className)
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) continue if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) continue
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Data>>) @Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Context>>)
} }
} }
/**
* A patch bundle of type [Jar].
*
* @param patchBundlePath The path to the patch bundle.
*/
class Jar(patchBundlePath: String) : PatchBundle(patchBundlePath) {
/**
* Load patches from the patch bundle.
*
* Patches will be loaded with a new [URLClassLoader].
*/
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", "")
}
)
}
/**
* A patch bundle of type [Dex] format.
*
* @param patchBundlePath The path to a patch bundle of dex format.
* @param dexClassLoader The dex class loader.
*/
class Dex(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) {
/**
* Load patches from the patch bundle.
*
* Patches will be loaded to the provided [dexClassLoader].
*/
fun loadPatches() = loadPatches(dexClassLoader,
StringIterator(DexFileFactory.loadDexFile(path, null).classes.iterator()) { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
})
}
} }

View File

@ -1,17 +0,0 @@
package app.revanced.patcher.util.patch.impl
import app.revanced.patcher.util.patch.PatchBundle
import app.revanced.patcher.util.patch.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

@ -1,30 +0,0 @@
package app.revanced.patcher.util.patch.impl
import app.revanced.patcher.util.patch.PatchBundle
import app.revanced.patcher.util.patch.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

@ -8,34 +8,26 @@ import org.jf.dexlib2.iface.ClassDef
* *
* A class proxy simply holds a reference to the original class * A class proxy simply holds a reference to the original class
* and allocates a mutable clone for the original class if needed. * and allocates a mutable clone for the original class if needed.
* @param immutableClass The class to proxy * @param immutableClass The class to proxy.
*/ */
class ClassProxy( class ClassProxy internal constructor(
val immutableClass: ClassDef, val immutableClass: ClassDef,
) { ) {
internal var proxyUsed = false /**
internal lateinit var mutatedClass: MutableClass * Weather the proxy was actually used.
*/
init { internal var resolved = false
// in the instance, that a [MutableClass] is being proxied,
// do not create an additional clone and reuse the [MutableClass] instance
if (immutableClass is MutableClass) {
mutatedClass = immutableClass
proxyUsed = true
}
}
/** /**
* Allocates and returns a mutable clone of the original class. * The mutable clone of the original class.
* A patch should always use the original immutable class reference *
* to avoid unnecessary allocations for the mutable class. * Note: This is only allocated if the proxy is actually used.
* @return A mutable clone of the original class.
*/ */
fun resolve(): MutableClass { val mutableClass by lazy {
if (!proxyUsed) { resolved = true
proxyUsed = true if (immutableClass is MutableClass) {
mutatedClass = MutableClass(immutableClass) immutableClass
} } else
return mutatedClass MutableClass(immutableClass)
} }
} }

View File

@ -3,18 +3,15 @@ package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.BytecodeData import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.patch.OptionsContainer import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
@ -39,11 +36,11 @@ import kotlin.io.path.Path
@Description("Example demonstration of a bytecode patch.") @Description("Example demonstration of a bytecode patch.")
@ExampleResourceCompatibility @ExampleResourceCompatibility
@Version("0.0.1") @Version("0.0.1")
@DependsOn([ExampleBytecodePatch::class]) @DependsOn([ExampleResourcePatch::class])
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// This function will be executed by the patcher. // This function will be executed by the patcher.
// You can treat it as a constructor // You can treat it as a constructor
override fun execute(data: BytecodeData): PatchResult { override fun execute(context: BytecodeContext): PatchResult {
// Get the resolved method by its fingerprint from the resolver cache // Get the resolved method by its fingerprint from the resolver cache
val result = ExampleFingerprint.result!! val result = ExampleFingerprint.result!!
@ -63,9 +60,9 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.") implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
// Get the class in which the method matching our fingerprint is defined in. // Get the class in which the method matching our fingerprint is defined in.
val mainClass = data.findClass { val mainClass = context.findClass {
it.type == result.classDef.type it.type == result.classDef.type
}!!.resolve() }!!.mutableClass
// Add a new method returning a string // Add a new method returning a string
mainClass.methods.add( mainClass.methods.add(
@ -169,6 +166,7 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
) )
} }
@Suppress("unused")
companion object : OptionsContainer() { companion object : OptionsContainer() {
private var key1 by option( private var key1 by option(
PatchOption.StringOption( PatchOption.StringOption(

View File

@ -3,11 +3,11 @@ package app.revanced.patcher.usage.resource.patch
import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.ResourceData import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.PatchResult import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.patch.ResourcePatch import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import org.w3c.dom.Element import org.w3c.dom.Element
@ -17,8 +17,8 @@ import org.w3c.dom.Element
@ExampleResourceCompatibility @ExampleResourceCompatibility
@Version("0.0.1") @Version("0.0.1")
class ExampleResourcePatch : ResourcePatch { class ExampleResourcePatch : ResourcePatch {
override fun execute(data: ResourceData): PatchResult { override fun execute(context: ResourceContext): PatchResult {
data.xmlEditor["AndroidManifest.xml"].use { editor -> context.xmlEditor["AndroidManifest.xml"].use { editor ->
val element = editor // regular DomFileEditor val element = editor // regular DomFileEditor
.file .file
.getElementsByTagName("application") .getElementsByTagName("application")

View File

@ -1,8 +1,7 @@
package app.revanced.patcher.util package app.revanced.patcher.util
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
internal class VersionReaderTest { internal class VersionReaderTest {
@Test @Test