mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 09:08:04 +01:00
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:
parent
d37452997b
commit
4aa14bbb85
20 changed files with 438 additions and 407 deletions
|
@ -1,19 +1,13 @@
|
|||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.Data
|
||||
import app.revanced.patcher.data.impl.findIndexed
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.data.findIndexed
|
||||
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
||||
import app.revanced.patcher.extensions.PatchExtensions.deprecated
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.extensions.nullOutputStream
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve
|
||||
import app.revanced.patcher.patch.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.patch.*
|
||||
import app.revanced.patcher.util.VersionReader
|
||||
import brut.androlib.Androlib
|
||||
import brut.androlib.meta.UsesFramework
|
||||
|
@ -29,10 +23,8 @@ import lanchon.multidexlib2.BasicDexFileNamer
|
|||
import lanchon.multidexlib2.DexIO
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import org.jf.dexlib2.Opcodes
|
||||
import org.jf.dexlib2.iface.ClassDef
|
||||
import org.jf.dexlib2.iface.DexFile
|
||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
|
@ -46,7 +38,7 @@ class Patcher(private val options: PatcherOptions) {
|
|||
private val logger = options.logger
|
||||
private val opcodes: Opcodes
|
||||
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
|
||||
val data: PatcherData
|
||||
val context: PatcherContext
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
@ -64,8 +56,8 @@ class Patcher(private val options: PatcherOptions) {
|
|||
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
||||
// get the opcodes
|
||||
opcodes = dexFile.opcodes
|
||||
// finally create patcher data
|
||||
data = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory)
|
||||
// finally create patcher context
|
||||
context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
|
||||
|
||||
// decode manifest file
|
||||
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) {
|
||||
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 (throwOnDuplicates) throw Exception("Class $type has already been added to the patcher")
|
||||
|
||||
logger.trace("Merging $type")
|
||||
data.bytecodeData.classes.internalClasses.add(classDef)
|
||||
context.bytecodeContext.classes.classes.add(classDef)
|
||||
modified = true
|
||||
|
||||
continue
|
||||
|
@ -104,7 +96,7 @@ class Patcher(private val options: PatcherOptions) {
|
|||
logger.trace("Overwriting $type")
|
||||
|
||||
val index = existingClass.second
|
||||
data.bytecodeData.classes.internalClasses[index] = classDef
|
||||
context.bytecodeContext.classes.classes[index] = classDef
|
||||
modified = true
|
||||
}
|
||||
if (modified) callback(file)
|
||||
|
@ -115,7 +107,7 @@ class Patcher(private val options: PatcherOptions) {
|
|||
* Save the patched dex file.
|
||||
*/
|
||||
fun save(): PatcherResult {
|
||||
val packageMetadata = data.packageMetadata
|
||||
val packageMetadata = context.packageMetadata
|
||||
val metaInfo = packageMetadata.metaInfo
|
||||
var resourceFile: File? = null
|
||||
|
||||
|
@ -168,14 +160,8 @@ class Patcher(private val options: PatcherOptions) {
|
|||
|
||||
logger.trace("Creating new dex file")
|
||||
val newDexFile = object : DexFile {
|
||||
override fun getClasses(): Set<ClassDef> {
|
||||
data.bytecodeData.classes.applyProxies()
|
||||
return ListBackedSet(data.bytecodeData.classes.internalClasses)
|
||||
}
|
||||
|
||||
override fun getOpcodes(): Opcodes {
|
||||
return this@Patcher.opcodes
|
||||
}
|
||||
override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
|
||||
override fun getOpcodes() = this@Patcher.opcodes
|
||||
}
|
||||
|
||||
// write modified dex files
|
||||
|
@ -199,12 +185,12 @@ class Patcher(private val options: PatcherOptions) {
|
|||
* Add [Patch]es to the patcher.
|
||||
* @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.
|
||||
* Note: Dependencies of the [Patch] will be cached as well.
|
||||
*/
|
||||
fun Class<out Patch<Data>>.isResource() {
|
||||
fun Class<out Patch<Context>>.isResource() {
|
||||
this.also {
|
||||
if (!ResourcePatch::class.java.isAssignableFrom(it)) return@also
|
||||
// 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() }
|
||||
}
|
||||
|
||||
data.patches.addAll(patches.onEach(Class<out Patch<Data>>::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)
|
||||
}
|
||||
}
|
||||
context.patches.addAll(patches.onEach(Class<out Patch<Context>>::isResource))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -310,7 +229,7 @@ class Patcher(private val options: PatcherOptions) {
|
|||
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
|
||||
|
||||
// read additional metadata from the resource table
|
||||
data.packageMetadata.let { metadata ->
|
||||
context.packageMetadata.let { metadata ->
|
||||
metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
|
||||
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
|
||||
data.packageMetadata.let { metadata ->
|
||||
context.packageMetadata.let { metadata ->
|
||||
metadata.packageName = resourceTable.currentResPackage.name
|
||||
metadata.packageVersion = resourceTable.versionInfo.versionName
|
||||
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.
|
||||
* @return A pair of the name of the [Patch] and its [PatchResult].
|
||||
*/
|
||||
fun applyPatches(stopOnError: Boolean = false) = sequence {
|
||||
// prevent from decoding the manifest twice if it is not needed
|
||||
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
|
||||
fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
|
||||
/**
|
||||
* 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 {
|
||||
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
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
} 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 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)
|
19
src/main/kotlin/app/revanced/patcher/PatcherContext.kt
Normal file
19
src/main/kotlin/app/revanced/patcher/PatcherContext.kt
Normal 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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.data.Data
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
@ -14,6 +14,6 @@ import kotlin.reflect.KClass
|
|||
@MustBeDocumented
|
||||
annotation class PatchDeprecated(
|
||||
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.
|
||||
)
|
|
@ -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 java.io.Closeable
|
||||
import java.io.File
|
||||
|
@ -11,7 +14,79 @@ import javax.xml.transform.TransformerFactory
|
|||
import javax.xml.transform.dom.DOMSource
|
||||
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()
|
||||
|
||||
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
|
||||
|
@ -23,7 +98,7 @@ class ResourceData(private val resourceCacheDirectory: File) : Data, Iterable<Fi
|
|||
DomFileEditor(inputStream)
|
||||
|
||||
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.
|
||||
* 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 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.
|
||||
* 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() {
|
||||
if (closed) return
|
||||
|
@ -100,4 +178,4 @@ class DomFileEditor internal constructor(
|
|||
// map of concurrent open files
|
||||
val locks = mutableMapOf<String, Int>()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
package app.revanced.patcher.extensions
|
||||
|
||||
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.patch.OptionsContainer
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchOptions
|
||||
import app.revanced.patcher.patch.annotations.DependsOn
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.companionObject
|
||||
|
@ -16,7 +18,7 @@ import kotlin.reflect.full.companionObjectInstance
|
|||
* @param targetAnnotation The annotation to find.
|
||||
* @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())
|
||||
|
||||
|
||||
|
@ -38,21 +40,34 @@ private fun <T : Annotation> Class<*>.findAnnotationRecursively(
|
|||
}
|
||||
|
||||
object PatchExtensions {
|
||||
val Class<*>.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>>.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<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.DependsOn::class)?.dependencies
|
||||
val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages
|
||||
val Class<out Patch<Data>>.options: PatchOptions?
|
||||
val Class<out Patch<Context>>.patchName: String
|
||||
get() = findAnnotationRecursively(Name::class)?.name ?: this.javaClass.simpleName
|
||||
|
||||
val Class<out Patch<Context>>.version
|
||||
get() = findAnnotationRecursively(Version::class)?.version
|
||||
|
||||
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 ->
|
||||
if (cl.visibility != KVisibility.PUBLIC) return null
|
||||
kotlin.companionObjectInstance?.let {
|
||||
(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 ->
|
||||
if (cl == Patch::class) null else cl
|
||||
}
|
||||
|
@ -61,9 +76,17 @@ object PatchExtensions {
|
|||
|
||||
object MethodFingerprintExtensions {
|
||||
val MethodFingerprint.name: String
|
||||
get() = javaClass.recursiveAnnotation(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.fuzzyPatternScanMethod get() = javaClass.recursiveAnnotation(app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod::class)
|
||||
val MethodFingerprint.fuzzyScanThreshold get() = fuzzyPatternScanMethod?.threshold ?: 0
|
||||
get() = javaClass.findAnnotationRecursively(Name::class)?.name ?: this.javaClass.simpleName
|
||||
|
||||
val MethodFingerprint.version
|
||||
get() = javaClass.findAnnotationRecursively(Version::class)?.version ?: "0.0.1"
|
||||
|
||||
val MethodFingerprint.description
|
||||
get() = javaClass.findAnnotationRecursively(Description::class)?.description
|
||||
|
||||
val MethodFingerprint.fuzzyPatternScanMethod
|
||||
get() = javaClass.findAnnotationRecursively(FuzzyPatternScanMethod::class)
|
||||
|
||||
val MethodFingerprint.fuzzyScanThreshold
|
||||
get() = fuzzyPatternScanMethod?.threshold ?: 0
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
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.fuzzyScanThreshold
|
||||
import app.revanced.patcher.extensions.parametersEqual
|
||||
|
@ -41,64 +41,67 @@ abstract class MethodFingerprint(
|
|||
companion object {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
classes@ for (classDef in context) // search through all classes for the fingerprint
|
||||
if (fingerprint.resolve(forData, classDef))
|
||||
classes@ for (classDef in classes) // search through all classes for the fingerprint
|
||||
if (fingerprint.resolve(context, classDef))
|
||||
break@classes // if the resolution succeeded, continue with the next fingerprint
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun MethodFingerprint.resolve(forData: BytecodeData, context: ClassDef): Boolean {
|
||||
for (method in context.methods)
|
||||
if (this.resolve(forData, method, context))
|
||||
fun MethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean {
|
||||
for (method in forClass.methods)
|
||||
if (this.resolve(context, method, forClass))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 forData The [BytecodeData] to host proxies.
|
||||
*
|
||||
* @param method The class on which to resolve the [MethodFingerprint] in.
|
||||
* @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.
|
||||
*/
|
||||
fun MethodFingerprint.resolve(forData: BytecodeData, context: Method, classDef: ClassDef): Boolean {
|
||||
fun MethodFingerprint.resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean {
|
||||
val methodFingerprint = this
|
||||
|
||||
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
|
||||
|
||||
if (methodFingerprint.access != null && methodFingerprint.access != context.accessFlags)
|
||||
if (methodFingerprint.access != null && methodFingerprint.access != method.accessFlags)
|
||||
return false
|
||||
|
||||
|
||||
if (methodFingerprint.parameters != null && !parametersEqual(
|
||||
methodFingerprint.parameters, // TODO: parseParameters()
|
||||
context.parameterTypes
|
||||
method.parameterTypes
|
||||
)
|
||||
) return false
|
||||
|
||||
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
|
||||
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(context))
|
||||
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method))
|
||||
return false
|
||||
|
||||
val stringsScanResult: StringsScanResult? =
|
||||
if (methodFingerprint.strings != null) {
|
||||
StringsScanResult(
|
||||
buildList {
|
||||
val implementation = context.implementation ?: return false
|
||||
val implementation = method.implementation ?: return false
|
||||
|
||||
val stringsList = methodFingerprint.strings.toMutableList()
|
||||
|
||||
|
@ -124,19 +127,19 @@ abstract class MethodFingerprint(
|
|||
} else 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
|
||||
|
||||
methodFingerprint.result = MethodFingerprintResult(
|
||||
context,
|
||||
classDef,
|
||||
method,
|
||||
forClass,
|
||||
MethodFingerprintResult.MethodFingerprintScanResult(
|
||||
patternScanResult,
|
||||
stringsScanResult
|
||||
),
|
||||
forData
|
||||
context
|
||||
)
|
||||
|
||||
return true
|
||||
|
@ -215,16 +218,17 @@ private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintS
|
|||
|
||||
/**
|
||||
* Represents the result of a [MethodFingerprintResult].
|
||||
*
|
||||
* @param method The matching method.
|
||||
* @param classDef The [ClassDef] that contains the matching [method].
|
||||
* @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(
|
||||
val method: Method,
|
||||
val classDef: ClassDef,
|
||||
val scanResult: MethodFingerprintScanResult,
|
||||
internal val data: BytecodeData
|
||||
internal val context: BytecodeContext
|
||||
) {
|
||||
|
||||
/**
|
||||
|
@ -283,7 +287,7 @@ data class MethodFingerprintResult(
|
|||
* Use [classDef] where possible.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val mutableClass by lazy { data.proxy(classDef).resolve() }
|
||||
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
||||
|
||||
/**
|
||||
* Returns a mutable clone of [method]
|
||||
|
|
|
@ -1,34 +1,44 @@
|
|||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.data.Data
|
||||
import app.revanced.patcher.data.impl.BytecodeData
|
||||
import app.revanced.patcher.data.impl.ResourceData
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import java.io.Closeable
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
interface ResourcePatch : Patch<ResourceData>
|
||||
interface ResourcePatch : Patch<ResourceContext>
|
||||
|
||||
/**
|
||||
* Bytecode patch for the Patcher.
|
||||
*
|
||||
* @param fingerprints A list of [MethodFingerprint] this patch relies on.
|
||||
*/
|
||||
abstract class BytecodePatch(
|
||||
internal val fingerprints: Iterable<MethodFingerprint>? = null
|
||||
) : Patch<BytecodeData>
|
||||
) : Patch<BytecodeContext>
|
|
@ -1,6 +1,6 @@
|
|||
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 kotlin.reflect.KClass
|
||||
|
||||
|
@ -20,5 +20,5 @@ annotation class Patch(val include: Boolean = true)
|
|||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
annotation class DependsOn(
|
||||
val dependencies: Array<KClass<out Patch<Data>>> = []
|
||||
val dependencies: Array<KClass<out Patch<Context>>> = []
|
||||
)
|
|
@ -3,40 +3,44 @@ package app.revanced.patcher.util
|
|||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import org.jf.dexlib2.iface.ClassDef
|
||||
|
||||
class ProxyBackedClassList(internal val internalClasses: MutableList<ClassDef>) : List<ClassDef> {
|
||||
private val internalProxies = mutableListOf<ClassProxy>()
|
||||
internal val proxies: List<ClassProxy> = internalProxies
|
||||
|
||||
fun add(classDef: ClassDef) = internalClasses.add(classDef)
|
||||
fun add(classProxy: ClassProxy) = internalProxies.add(classProxy)
|
||||
/**
|
||||
* A class that represents a set of classes and proxies.
|
||||
*
|
||||
* @param classes The classes to be backed by proxies.
|
||||
*/
|
||||
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() {
|
||||
// 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
|
||||
fun add(classDef: ClassDef) = classes.add(classDef)
|
||||
|
||||
// if it has been used, replace the internal class which it proxied
|
||||
val index = internalClasses.indexOfFirst { it.type == proxy.immutableClass.type }
|
||||
internalClasses[index] = proxy.mutatedClass
|
||||
/**
|
||||
* Add a [ClassProxy].
|
||||
*/
|
||||
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@removeIf true
|
||||
}
|
||||
}
|
||||
|
||||
override val size get() = internalClasses.size
|
||||
override fun contains(element: ClassDef) = internalClasses.contains(element)
|
||||
override fun containsAll(elements: Collection<ClassDef>) = internalClasses.containsAll(elements)
|
||||
override fun get(index: Int) = internalClasses[index]
|
||||
override fun indexOf(element: ClassDef) = internalClasses.indexOf(element)
|
||||
override fun isEmpty() = internalClasses.isEmpty()
|
||||
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)
|
||||
|
||||
override val size get() = classes.size
|
||||
override fun contains(element: ClassDef) = classes.contains(element)
|
||||
override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
|
||||
override fun isEmpty() = classes.isEmpty()
|
||||
override fun iterator() = classes.iterator()
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package app.revanced.patcher.util.method
|
||||
|
||||
import app.revanced.patcher.data.impl.BytecodeData
|
||||
import app.revanced.patcher.data.impl.MethodNotFoundException
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.extensions.softCompareTo
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
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.
|
||||
* @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.
|
||||
*/
|
||||
class MethodWalker internal constructor(
|
||||
private val bytecodeData: BytecodeData,
|
||||
private val bytecodeContext: BytecodeContext,
|
||||
private var currentMethod: Method
|
||||
) {
|
||||
/**
|
||||
* Get the method which was walked last.
|
||||
*
|
||||
* 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 {
|
||||
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.
|
||||
*
|
||||
* The current method will be mutable.
|
||||
*
|
||||
* @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.
|
||||
* The current method will be mutable.
|
||||
* @return The same [MethodWalker] instance with the method at [offset].
|
||||
*/
|
||||
fun nextMethod(offset: Int, walkMutable: Boolean = false): MethodWalker {
|
||||
currentMethod.implementation?.instructions?.let { instructions ->
|
||||
val instruction = instructions.elementAt(offset)
|
||||
|
||||
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 ->
|
||||
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}")
|
||||
}
|
||||
|
||||
|
||||
internal class MethodNotFoundException(exception: String) : Exception(exception)
|
||||
}
|
|
@ -1,18 +1,74 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
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 org.jf.dexlib2.DexFileFactory
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarFile
|
||||
|
||||
/**
|
||||
* A 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 {
|
||||
for (className in classNames) {
|
||||
val clazz = classLoader.loadClass(className)
|
||||
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('/', '.')
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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('/', '.')
|
||||
})
|
||||
}
|
|
@ -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", "")
|
||||
}
|
||||
)
|
||||
}
|
|
@ -8,34 +8,26 @@ import org.jf.dexlib2.iface.ClassDef
|
|||
*
|
||||
* A class proxy simply holds a reference to the original class
|
||||
* 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,
|
||||
) {
|
||||
internal var proxyUsed = false
|
||||
internal lateinit var mutatedClass: MutableClass
|
||||
|
||||
init {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Weather the proxy was actually used.
|
||||
*/
|
||||
internal var resolved = false
|
||||
|
||||
/**
|
||||
* Allocates and returns a mutable clone of the original class.
|
||||
* A patch should always use the original immutable class reference
|
||||
* to avoid unnecessary allocations for the mutable class.
|
||||
* @return A mutable clone of the original class.
|
||||
* The mutable clone of the original class.
|
||||
*
|
||||
* Note: This is only allocated if the proxy is actually used.
|
||||
*/
|
||||
fun resolve(): MutableClass {
|
||||
if (!proxyUsed) {
|
||||
proxyUsed = true
|
||||
mutatedClass = MutableClass(immutableClass)
|
||||
}
|
||||
return mutatedClass
|
||||
val mutableClass by lazy {
|
||||
resolved = true
|
||||
if (immutableClass is MutableClass) {
|
||||
immutableClass
|
||||
} else
|
||||
MutableClass(immutableClass)
|
||||
}
|
||||
}
|
|
@ -3,18 +3,15 @@ package app.revanced.patcher.usage.bytecode
|
|||
import app.revanced.patcher.annotation.Description
|
||||
import app.revanced.patcher.annotation.Name
|
||||
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.or
|
||||
import app.revanced.patcher.extensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.OptionsContainer
|
||||
import app.revanced.patcher.patch.PatchOption
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import app.revanced.patcher.patch.PatchResultSuccess
|
||||
import app.revanced.patcher.patch.*
|
||||
import app.revanced.patcher.patch.annotations.DependsOn
|
||||
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.patch.ExampleResourcePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import com.google.common.collect.ImmutableList
|
||||
|
@ -39,11 +36,11 @@ import kotlin.io.path.Path
|
|||
@Description("Example demonstration of a bytecode patch.")
|
||||
@ExampleResourceCompatibility
|
||||
@Version("0.0.1")
|
||||
@DependsOn([ExampleBytecodePatch::class])
|
||||
@DependsOn([ExampleResourcePatch::class])
|
||||
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
||||
// This function will be executed by the patcher.
|
||||
// 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
|
||||
val result = ExampleFingerprint.result!!
|
||||
|
||||
|
@ -63,9 +60,9 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
|||
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
|
||||
|
||||
// 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
|
||||
}!!.resolve()
|
||||
}!!.mutableClass
|
||||
|
||||
// Add a new method returning a string
|
||||
mainClass.methods.add(
|
||||
|
@ -169,6 +166,7 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
companion object : OptionsContainer() {
|
||||
private var key1 by option(
|
||||
PatchOption.StringOption(
|
||||
|
|
|
@ -3,11 +3,11 @@ package app.revanced.patcher.usage.resource.patch
|
|||
import app.revanced.patcher.annotation.Description
|
||||
import app.revanced.patcher.annotation.Name
|
||||
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.PatchResultSuccess
|
||||
import app.revanced.patcher.patch.annotations.Patch
|
||||
import app.revanced.patcher.patch.ResourcePatch
|
||||
import app.revanced.patcher.patch.annotations.Patch
|
||||
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
||||
import org.w3c.dom.Element
|
||||
|
||||
|
@ -17,8 +17,8 @@ import org.w3c.dom.Element
|
|||
@ExampleResourceCompatibility
|
||||
@Version("0.0.1")
|
||||
class ExampleResourcePatch : ResourcePatch {
|
||||
override fun execute(data: ResourceData): PatchResult {
|
||||
data.xmlEditor["AndroidManifest.xml"].use { editor ->
|
||||
override fun execute(context: ResourceContext): PatchResult {
|
||||
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
||||
val element = editor // regular DomFileEditor
|
||||
.file
|
||||
.getElementsByTagName("application")
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
package app.revanced.patcher.util
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class VersionReaderTest {
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue