1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-13 18:30:49 +02:00

Compare commits

...

11 Commits

Author SHA1 Message Date
semantic-release-bot
1795f376ef chore(release): 14.0.0-dev.2 [skip ci]
# [14.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0-dev.1...v14.0.0-dev.2) (2023-08-19)
2023-08-19 15:26:18 +00:00
oSumAtrIX
e7360a7692 build(Needs bump): Bump dependencies
This fixes an issue with a library not working on Android
2023-08-19 17:23:31 +02:00
semantic-release-bot
e1fc86934f chore(release): 14.0.0-dev.1 [skip ci]
# [14.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0...v14.0.0-dev.1) (2023-08-18)

### Bug Fixes

* log decoding resources after logging deleting resource cache directory ([db62a16](db62a1607b))

### Code Refactoring

* improve structure and public API ([6b8977f](6b8977f178))

### BREAKING CHANGES

* Various public APIs have been changed. The `Version` annotation has been removed. Patches do not return anything anymore and instead throw `PatchException`. Multiple patch bundles can now be loaded in a single ClassLoader to bypass class loader isolation.
2023-08-18 23:47:18 +00:00
oSumAtrIX
6b8977f178 refactor: improve structure and public API
This commit introduces a couple changes besides the refactor. Executing patches can be cancelled, multiple bundles loaded into the same class loader and `Patch.execute` does not have to return anymore.

BREAKING CHANGE: Various public APIs have been changed. The `Version` annotation has been removed. Patches do not return anything anymore and instead throw `PatchException`. Multiple patch bundles can now be loaded in a single ClassLoader to bypass class loader isolation.
2023-08-19 01:45:27 +02:00
oSumAtrIX
12c6c73de0 build: add mavenLocal to repositories 2023-08-16 16:53:47 +02:00
oSumAtrIX
db62a1607b fix: log decoding resources after logging deleting resource cache directory 2023-08-16 16:53:45 +02:00
semantic-release-bot
58bb879ef5 chore(release): 13.0.0 [skip ci]
# [13.0.0](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1...v13.0.0) (2023-08-14)

### Bug Fixes

* decode in correct order ([8fb2f2d](8fb2f2dc1d))
* disable correct loggers ([c2d89c6](c2d89c622e))
* get framework ids to compile resources ([f2cb7ee](f2cb7ee7df))
* only enable logging for ReVanced ([783ccf8](783ccf8529))
* set package metadata correctly ([02d6ff1](02d6ff15fe))

* build(Needs bump)!: Bump dependencies ([d5f89a9](d5f89a903f))

### BREAKING CHANGES

* This bump updates smali, a crucial dependency
2023-08-14 02:11:55 +00:00
oSumAtrIX
254912438a chore: merge branch dev to main (#213) 2023-08-14 04:10:17 +02:00
semantic-release-bot
0e48918bcc chore(release): 13.0.0-dev.3 [skip ci]
# [13.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0-dev.2...v13.0.0-dev.3) (2023-08-14)

### Bug Fixes

* decode in correct order ([8fb2f2d](8fb2f2dc1d))
* only enable logging for ReVanced ([783ccf8](783ccf8529))
2023-08-14 02:04:54 +00:00
oSumAtrIX
783ccf8529 fix: only enable logging for ReVanced 2023-08-14 04:02:39 +02:00
oSumAtrIX
8fb2f2dc1d fix: decode in correct order 2023-08-14 04:02:24 +02:00
33 changed files with 857 additions and 806 deletions

View File

@@ -1,3 +1,49 @@
# [14.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0-dev.1...v14.0.0-dev.2) (2023-08-19)
# [14.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0...v14.0.0-dev.1) (2023-08-18)
### Bug Fixes
* log decoding resources after logging deleting resource cache directory ([db62a16](https://github.com/ReVanced/revanced-patcher/commit/db62a1607b4a9d6256b5f5153decb088d9680553))
### Code Refactoring
* improve structure and public API ([6b8977f](https://github.com/ReVanced/revanced-patcher/commit/6b8977f17854ef0344d868e6391cb18134eceadc))
### BREAKING CHANGES
* Various public APIs have been changed. The `Version` annotation has been removed. Patches do not return anything anymore and instead throw `PatchException`. Multiple patch bundles can now be loaded in a single ClassLoader to bypass class loader isolation.
# [13.0.0](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1...v13.0.0) (2023-08-14)
### Bug Fixes
* decode in correct order ([8fb2f2d](https://github.com/ReVanced/revanced-patcher/commit/8fb2f2dc1d3b9b1e9fd13b39485985d2886d52ae))
* disable correct loggers ([c2d89c6](https://github.com/ReVanced/revanced-patcher/commit/c2d89c622e06e58e5042e1a00ef67cee8a246e53))
* get framework ids to compile resources ([f2cb7ee](https://github.com/ReVanced/revanced-patcher/commit/f2cb7ee7dffa573c31df497cf235a3f5d120f91f))
* only enable logging for ReVanced ([783ccf8](https://github.com/ReVanced/revanced-patcher/commit/783ccf8529f5d16aa463982da6977328306232bb))
* set package metadata correctly ([02d6ff1](https://github.com/ReVanced/revanced-patcher/commit/02d6ff15fe87c2352de29749610e9d72db8ba418))
* build(Needs bump)!: Bump dependencies ([d5f89a9](https://github.com/ReVanced/revanced-patcher/commit/d5f89a903f019c199bdb27a50287124fc4b4978e))
### BREAKING CHANGES
* This bump updates smali, a crucial dependency
# [13.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0-dev.2...v13.0.0-dev.3) (2023-08-14)
### Bug Fixes
* decode in correct order ([8fb2f2d](https://github.com/ReVanced/revanced-patcher/commit/8fb2f2dc1d3b9b1e9fd13b39485985d2886d52ae))
* only enable logging for ReVanced ([783ccf8](https://github.com/ReVanced/revanced-patcher/commit/783ccf8529f5d16aa463982da6977328306232bb))
# [13.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0-dev.1...v13.0.0-dev.2) (2023-08-12) # [13.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0-dev.1...v13.0.0-dev.2) (2023-08-12)

View File

@@ -11,6 +11,7 @@ val githubPassword: String = project.findProperty("gpr.key") as? String ?: Syste
repositories { repositories {
mavenCentral() mavenCentral()
google() google()
mavenLocal()
listOf("multidexlib2", "apktool").forEach { repo -> listOf("multidexlib2", "apktool").forEach { repo ->
maven { maven {
url = uri("https://maven.pkg.github.com/revanced/$repo") url = uri("https://maven.pkg.github.com/revanced/$repo")
@@ -23,12 +24,15 @@ repositories {
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("xpp3:xpp3:1.1.4c") implementation("xpp3:xpp3:1.1.4c")
implementation("com.android.tools.smali:smali:3.0.3") implementation("com.android.tools.smali:smali:3.0.3")
implementation("app.revanced:multidexlib2:3.0.3.r2") implementation("app.revanced:multidexlib2:3.0.3.r2")
implementation("app.revanced:apktool-lib:2.8.2-3") implementation("app.revanced:apktool-lib:2.8.2-4")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
compileOnly("com.google.android:android:4.1.1.4")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
} }

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 13.0.0-dev.2 version = 14.0.0-dev.2

View File

@@ -0,0 +1,8 @@
package app.revanced.patcher
import java.io.File
@FunctionalInterface
interface IntegrationsConsumer {
fun acceptIntegrations(integrations: List<File>)
}

View File

@@ -1,15 +1,14 @@
package app.revanced.patcher.data package app.revanced.patcher
import brut.androlib.apk.ApkInfo import brut.androlib.apk.ApkInfo
/** /**
* Metadata about a package. * Metadata about a package.
*/ */
class PackageMetadata { class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) {
lateinit var packageName: String lateinit var packageName: String
internal set internal set
lateinit var packageVersion: String lateinit var packageVersion: String
internal set internal set
internal lateinit var apkInfo: ApkInfo
} }

View File

@@ -0,0 +1,78 @@
@file:Suppress("unused")
package app.revanced.patcher
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchClass
import dalvik.system.PathClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
/**
* A patch bundle.
*
* @param fromClasses The classes to get [Patch]es from.
*/
sealed class PatchBundleLoader private constructor(
fromClasses: Iterable<Class<*>>
) : MutableList<PatchClass> by mutableListOf() {
init {
fromClasses.filter {
if (it.isAnnotation) return@filter false
it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null
}.map {
@Suppress("UNCHECKED_CAST")
it as PatchClass
}.let { addAll(it) }
}
/**
* A [PatchBundleLoader] for JAR files.
*
* @param patchBundles The path to patch bundles of JAR format.
*/
class Jar(private vararg val patchBundles: File) : PatchBundleLoader(
with(URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray())) {
patchBundles.flatMap { patchBundle ->
// Get the names of all classes in the DEX file.
JarFile(patchBundle).entries().asSequence()
.filter { it.name.endsWith(".class") }
.map {
loadClass(it.name.replace('/', '.').replace(".class", ""))
}
}
}
)
/**
* A [PatchBundleLoader] for [Dex] files.
*
* @param patchBundlesPath The path to or a path to a directory containing patch bundles of DEX format.
*/
class Dex(private val patchBundlesPath: File) : PatchBundleLoader(
with(PathClassLoader(patchBundlesPath.absolutePath, null)) {
fun readDexFile(file: File) = MultiDexIO.readDexFile(
true,
file,
BasicDexFileNamer(),
null,
null
)
// Get the names of all classes in the DEX file.
val dexFiles = if (patchBundlesPath.isFile) listOf(readDexFile(patchBundlesPath))
else patchBundlesPath.listFiles { it -> it.isFile }?.map { readDexFile(it) } ?: emptyList()
dexFiles.flatMap { it.classes }.map { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
}.map { loadClass(it) }
}
)
}

View File

@@ -0,0 +1,8 @@
package app.revanced.patcher
import app.revanced.patcher.patch.PatchResult
import kotlinx.coroutines.flow.Flow
import java.util.function.Function
@FunctionalInterface
interface PatchExecutorFunction : Function<Boolean, Flow<PatchResult>>

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +1,37 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.* import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.logging.Logger import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.patch.PatchClass
import com.android.tools.smali.dexlib2.iface.ClassDef import brut.androlib.apk.ApkInfo
import java.io.File import brut.directory.ExtFile
data class PatcherContext(
val classes: MutableList<ClassDef>,
val resourceCacheDirectory: File,
) {
val packageMetadata = PackageMetadata()
internal val patches = mutableListOf<Class<out Patch<Context>>>()
internal val integrations = Integrations(this)
internal val bytecodeContext = BytecodeContext(classes)
internal val resourceContext = ResourceContext(resourceCacheDirectory)
internal class Integrations(val context: PatcherContext) {
var callback: ((File) -> Unit)? = null
private val integrations: MutableList<File> = mutableListOf()
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
/** /**
* Merge integrations. * A context for ReVanced [Patcher].
* @param logger A logger. *
* @param options The [PatcherOptions] used to create this context.
*/ */
fun merge(logger: Logger) { class PatcherContext internal constructor(options: PatcherOptions) {
with(context.bytecodeContext.classes) { /**
for (integrations in integrations) { * [PackageMetadata] of the supplied [PatcherOptions.inputFile].
callback?.let { it(integrations) } */
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile)))
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile( /**
true, * The list of [Patch]es to execute.
integrations, */
NAMER, internal val patches = mutableListOf<PatchClass>()
null,
null
).classes) {
val type = classDef.type
val result = classes.findIndexed { it.type == type } /**
if (result == null) { * The [ResourceContext] of this [PatcherContext].
logger.trace("Merging type $type") * This holds the current state of the resources.
classes.add(classDef) */
continue internal val resourceContext = ResourceContext(this, options)
}
val (existingClass, existingClassIndex) = result /**
* The [BytecodeContext] of this [PatcherContext].
logger.trace("Type $type exists. Adding missing methods and fields.") * This holds the current state of the bytecode.
*/
existingClass.merge(classDef, context, logger).let { mergedClass -> internal val bytecodeContext = BytecodeContext(options)
if (mergedClass !== existingClass) // referential equality check
classes[existingClassIndex] = mergedClass
}
}
}
}
}
}
} }

View File

@@ -1,25 +1,42 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.logging.Logger import app.revanced.patcher.logging.Logger
import app.revanced.patcher.logging.impl.NopLogger import app.revanced.patcher.logging.impl.NopLogger
import brut.androlib.Config
import java.io.File import java.io.File
/** /**
* Options for the [Patcher]. * Options for ReVanced [Patcher].
* @param inputFile The input file (usually an apk file). * @param inputFile The input file to patch.
* @param resourceCacheDirectory Directory to cache resources. * @param resourceCachePath The path to the directory to use for caching resources.
* @param aaptPath Optional path to a custom aapt binary. * @param aaptBinaryPath The path to a custom aapt binary.
* @param frameworkDirectory Optional path to a custom framework directory. * @param frameworkFileDirectory The path to the directory to cache the framework file in.
* @param logger Custom logger implementation for the [Patcher]. * @param logger A [Logger].
*/ */
data class PatcherOptions( data class PatcherOptions(
internal val inputFile: File, internal val inputFile: File,
internal val resourceCacheDirectory: String, internal val resourceCachePath: File = File("revanced-resource-cache"),
internal val aaptPath: String? = null, internal val aaptBinaryPath: String? = null,
internal val frameworkDirectory: String? = null, internal val frameworkFileDirectory: String? = null,
internal val logger: Logger = NopLogger internal val logger: Logger = NopLogger
) { ) {
fun recreateResourceCacheDirectory() = File(resourceCacheDirectory).also { /**
* The mode to use for resource decoding.
* @see ResourceContext.ResourceDecodingMode
*/
internal var resourceDecodingMode = ResourceContext.ResourceDecodingMode.MANIFEST_ONLY
/**
* The configuration to use for resource decoding and compiling.
*/
internal val resourceConfig = Config.getDefaultConfig().apply {
useAapt2 = true
aaptPath = aaptBinaryPath ?: ""
frameworkDirectory = frameworkFileDirectory
}
fun recreateResourceCacheDirectory() = resourceCachePath.also {
if (it.exists()) { if (it.exists()) {
logger.info("Deleting existing resource cache directory") logger.info("Deleting existing resource cache directory")

View File

@@ -1,16 +1,23 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.util.dex.DexFile
import java.io.File import java.io.File
import java.io.InputStream
/** /**
* The result of a patcher. * The result of a patcher.
* @param dexFiles The patched dex files. * @param dexFiles The patched dex files.
* @param doNotCompress List of relative paths to files to exclude from compressing.
* @param resourceFile File containing resources that need to be extracted into the APK. * @param resourceFile File containing resources that need to be extracted into the APK.
* @param doNotCompress List of relative paths of files to exclude from compressing.
*/ */
data class PatcherResult( data class PatcherResult(
val dexFiles: List<DexFile>, val dexFiles: List<PatchedDexFile>,
val doNotCompress: List<String>? = null, val resourceFile: File?,
val resourceFile: File? val doNotCompress: List<String>? = null
) ) {
/**
* Wrapper for dex files.
* @param name The original name of the dex file.
* @param stream The dex file as [InputStream].
*/
class PatchedDexFile(val name: String, val stream: InputStream)
}

View File

@@ -0,0 +1,8 @@
package app.revanced.patcher
import app.revanced.patcher.patch.PatchClass
@FunctionalInterface
interface PatchesConsumer {
fun acceptPatches(patches: List<PatchClass>)
}

View File

@@ -19,14 +19,3 @@ annotation class Name(
annotation class Description( annotation class Description(
val description: String, val description: String,
) )
/**
* Annotation to version a [Patch].
* @param version The version of a [Patch].
*/
@Target(AnnotationTarget.CLASS)
@Deprecated("This annotation is deprecated and will be removed in a future release.")
annotation class Version(
val version: String,
)

View File

@@ -0,0 +1,152 @@
package app.revanced.patcher.data
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.ProxyClassList
import app.revanced.patcher.util.method.MethodWalker
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.io.Flushable
/**
* A context for bytecode.
* This holds the current state of the bytecode.
*
* @param options The [PatcherOptions] used to create this context.
*/
class BytecodeContext internal constructor(private val options: PatcherOptions) :
Context<List<PatcherResult.PatchedDexFile>> {
/**
* [Opcodes] of the supplied [PatcherOptions.inputFile].
*/
internal lateinit var opcodes: Opcodes
/**
* The list of classes.
*/
val classes by lazy {
ProxyClassList(
MultiDexIO.readDexFile(
true, options.inputFile, BasicDexFileNamer(), null, null
).also { opcodes = it.opcodes }.classes.toMutableSet()
)
}
/**
* The [Integrations] of this [PatcherContext].
*/
internal val integrations = Integrations(options.logger)
/**
* 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) }
/**
* Proxy a class.
* This will allow the class to be modified.
*
* @param classDef The class to proxy.
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) = this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
ClassProxy(classDef).also { this.classes.add(it) }
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
/**
* The integrations of a [PatcherContext].
*
* @param logger The logger to use.
*/
internal inner class Integrations(private val logger: Logger) : MutableList<File> by mutableListOf(), Flushable {
/**
* Whether to merge integrations.
* True when any supplied [Patch] is annotated with [RequiresIntegrations].
*/
var merge = false
/**
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
*/
override fun flush() {
if (!merge) return
this@Integrations.forEach { integrations ->
MultiDexIO.readDexFile(
true,
integrations, BasicDexFileNamer(),
null,
null
).classes.forEach classDef@{ classDef ->
val existingClass = classes.find { it == classDef } ?: run {
logger.trace("Merging $classDef")
classes.add(classDef)
return@classDef
}
logger.trace("$classDef exists. Adding missing methods and fields.")
existingClass.merge(classDef, this@BytecodeContext, logger).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) return@let
classes.apply { remove(existingClass); add(mergedClass) }
}
}
}
clear()
}
}
/**
* Compile bytecode from the [BytecodeContext].
*
* @return The compiled bytecode.
*/
override fun get(): List<PatcherResult.PatchedDexFile> {
options.logger.info("Compiling modified dex files")
return mutableMapOf<String, MemoryDataStore>().apply {
MultiDexIO.writeDexFile(
true, -1, // Defaults to amount of available cores.
this, BasicDexFileNamer(), object : DexFile {
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
override fun getOpcodes() = this@BytecodeContext.opcodes
}, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
)
}.map { PatcherResult.PatchedDexFile(it.key, it.value.readAt(0)) }
}
}

View File

@@ -1,170 +1,9 @@
package app.revanced.patcher.data package app.revanced.patcher.data
import app.revanced.patcher.util.ProxyBackedClassList import java.util.function.Supplier
import app.revanced.patcher.util.method.MethodWalker
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/** /**
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext]. * A common interface for contexts such as [ResourceContext] and [BytecodeContext].
*/ */
sealed interface Context sealed interface Context<T> : Supplier<T>
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
}
}
/**
* 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)
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
inner class XmlFileHolder {
operator fun get(inputStream: InputStream) =
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path])
}
}
}
/**
* Wrapper for a file that can be edited as a dom document.
*
* 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.
*
*/
class DomFileEditor internal constructor(
private val inputStream: InputStream,
private val outputStream: Lazy<OutputStream>? = null,
) : Closeable {
// path to the xml file to unlock the resource when closing the editor
private var filePath: String? = null
private var closed: Boolean = false
/**
* The document of the xml file
*/
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
// lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
// increase the lock
locks.merge(file.path, 1, Integer::sum)
filePath = file.path
}
/**
* Closes the editor. Write backs and decreases the lock count.
*
* Will not write back to the file if the file is still locked.
*/
override fun close() {
if (closed) return
inputStream.close()
// if the output stream is not null, do not close it
outputStream?.let {
// prevent writing to same file, if it is being locked
// isLocked will be false if the editor was created through a stream
val isLocked = filePath?.let { path ->
val isLocked = locks[path]!! > 1
// decrease the lock count if the editor was opened for a file
locks.merge(path, -1, Integer::sum)
isLocked
} ?: false
// if unlocked, write back to the file
if (!isLocked) {
it.value.use { stream ->
val result = StreamResult(stream)
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
}
it.value.close()
return
}
}
closed = true
}
private companion object {
// map of concurrent open files
val locks = mutableMapOf<String, Int>()
}
}

View File

@@ -0,0 +1,157 @@
package app.revanced.patcher.data
import app.revanced.patcher.PatcherContext
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.util.DomFileEditor
import brut.androlib.AaptInvoker
import brut.androlib.ApkDecoder
import brut.androlib.apk.UsesFramework
import brut.androlib.res.Framework
import brut.androlib.res.ResourcesDecoder
import brut.androlib.res.decoder.AndroidManifestResourceParser
import brut.androlib.res.decoder.XmlPullStreamDecoder
import brut.androlib.res.xml.ResXmlPatcher
import brut.directory.ExtFile
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
/**
* A context for resources.
* This holds the current state of the resources.
*
* @param context The [PatcherContext] to create the context for.
*/
class ResourceContext internal constructor(
private val context: PatcherContext,
private val options: PatcherOptions
) : Context<File?>, Iterable<File> {
val xmlEditor = XmlFileHolder()
/**
* Decode resources for the patcher.
*
* @param mode The [ResourceDecodingMode] to use when decoding.
*/
internal fun decodeResources(mode: ResourceDecodingMode) = with(context.packageMetadata.apkInfo) {
// Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this)
when (mode) {
ResourceDecodingMode.FULL -> {
val outDir = options.recreateResourceCacheDirectory()
options.logger.info("Decoding resources")
resourcesDecoder.decodeResources(outDir)
resourcesDecoder.decodeManifest(outDir)
// Needed to record uncompressed files.
val apkDecoder = ApkDecoder(options.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework = UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
}
ResourceDecodingMode.MANIFEST_ONLY -> {
options.logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
XmlPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer
).decodeManifest(
apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { /* do nothing */
}
}
)
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
context.packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode
}
}
}
}
}
operator fun get(path: String) = options.resourceCachePath.resolve(path)
override fun iterator() = options.resourceCachePath.walkTopDown().iterator()
/**
* Compile resources from the [ResourceContext].
*
* @return The compiled resources.
*/
override fun get(): File? {
var resourceFile: File? = null
if (options.resourceDecodingMode == ResourceDecodingMode.FULL) {
options.logger.info("Compiling modified resources")
val cacheDirectory = ExtFile(options.resourceCachePath)
val aaptFile = cacheDirectory.resolve("aapt_temp_file").also {
Files.deleteIfExists(it.toPath())
}.also { resourceFile = it }
try {
AaptInvoker(
options.resourceConfig, context.packageMetadata.apkInfo
).invokeAapt(aaptFile,
cacheDirectory.resolve("AndroidManifest.xml").also {
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
},
cacheDirectory.resolve("res"),
null,
null,
context.packageMetadata.apkInfo.usesFramework.let { usesFramework ->
usesFramework.ids.map { id ->
Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag)
}.toTypedArray()
})
} finally {
cacheDirectory.close()
}
}
return resourceFile
}
/**
* The type of decoding the resources.
*/
internal enum class ResourceDecodingMode {
/**
* Decode all resources.
*/
FULL,
/**
* Decode the manifest file only.
*/
MANIFEST_ONLY,
}
inner class XmlFileHolder {
operator fun get(inputStream: InputStream) =
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path])
}
}
}

View File

@@ -22,8 +22,7 @@ object InstructionExtensions {
fun MutableMethodImplementation.addInstructions( fun MutableMethodImplementation.addInstructions(
index: Int, index: Int,
instructions: List<BuilderInstruction> instructions: List<BuilderInstruction>
) = ) = instructions.asReversed().forEach { addInstruction(index, it) }
instructions.asReversed().forEach { addInstruction(index, it) }
/** /**
* Add instructions to a method. * Add instructions to a method.

View File

@@ -3,11 +3,10 @@ package app.revanced.patcher.extensions
import app.revanced.patcher.annotation.Compatibility import app.revanced.patcher.annotation.Compatibility
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.data.Context
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
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.PatchClass
import app.revanced.patcher.patch.PatchOptions import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.RequiresIntegrations import app.revanced.patcher.patch.annotations.RequiresIntegrations
@@ -19,50 +18,43 @@ object PatchExtensions {
/** /**
* The name of a [Patch]. * The name of a [Patch].
*/ */
val Class<out Patch<Context>>.patchName: String val PatchClass.patchName: String
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
/**
* The version of a [Patch].
*/
@Deprecated("This property is deprecated and will be removed in a future release.")
val Class<out Patch<Context>>.version
get() = findAnnotationRecursively(Version::class)?.version
/** /**
* Weather or not a [Patch] should be included. * Weather or not a [Patch] should be included.
*/ */
val Class<out Patch<Context>>.include val PatchClass.include
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
/** /**
* The description of a [Patch]. * The description of a [Patch].
*/ */
val Class<out Patch<Context>>.description val PatchClass.description
get() = findAnnotationRecursively(Description::class)?.description get() = findAnnotationRecursively(Description::class)?.description
/** /**
* The dependencies of a [Patch]. * The dependencies of a [Patch].
*/ */
val Class<out Patch<Context>>.dependencies val PatchClass.dependencies
get() = findAnnotationRecursively(DependsOn::class)?.dependencies get() = findAnnotationRecursively(DependsOn::class)?.dependencies
/** /**
* The packages a [Patch] is compatible with. * The packages a [Patch] is compatible with.
*/ */
val Class<out Patch<Context>>.compatiblePackages val PatchClass.compatiblePackages
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
/** /**
* Weather or not a [Patch] requires integrations. * Weather or not a [Patch] requires integrations.
*/ */
internal val Class<out Patch<Context>>.requiresIntegrations internal val PatchClass.requiresIntegrations
get() = findAnnotationRecursively(RequiresIntegrations::class) != null get() = findAnnotationRecursively(RequiresIntegrations::class) != null
/** /**
* The options of a [Patch]. * The options of a [Patch].
*/ */
val Class<out Patch<Context>>.options: PatchOptions? val PatchClass.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 {

View File

@@ -4,7 +4,7 @@ import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.Fingerprint import app.revanced.patcher.fingerprint.Fingerprint
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.patch.PatchResultError import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.ClassProxy import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@@ -99,9 +99,9 @@ abstract class MethodFingerprint(
methodClassPairs!!.add(methodClassPair) methodClassPairs!!.add(methodClassPair)
} }
if (methods.isNotEmpty()) MethodFingerprint.clearFingerprintResolutionLookupMaps() if (methods.isNotEmpty()) clearFingerprintResolutionLookupMaps()
context.classes.classes.forEach { classDef -> context.classes.forEach { classDef ->
classDef.methods.forEach { method -> classDef.methods.forEach { method ->
val methodClassPair = method to classDef val methodClassPair = method to classDef
@@ -160,7 +160,7 @@ abstract class MethodFingerprint(
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*/ */
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) { internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
if (methods.isEmpty()) throw PatchResultError("lookup map not initialized") if (methods.isEmpty()) throw PatchException("lookup map not initialized")
for (fingerprint in this) { for (fingerprint in this) {
fingerprint.resolveUsingLookupMap(context) fingerprint.resolveUsingLookupMap(context)

View File

@@ -6,20 +6,22 @@ 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
typealias PatchClass = Class<out Patch<Context<*>>>
/** /**
* A ReVanced patch. * A ReVanced patch.
* *
* 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 : Context> { sealed interface Patch<out T : Context<*>> {
/** /**
* 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. * @param context The [Context] the patch will work on.
* @return The result of executing the patch. * @return The result of executing the patch.
*/ */
fun execute(context: @UnsafeVariance T): PatchResult fun execute(context: @UnsafeVariance T)
} }
/** /**

View File

@@ -0,0 +1,12 @@
package app.revanced.patcher.patch
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Throwable) : this(cause.message, cause)
}

View File

@@ -1,35 +1,10 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
interface PatchResult { /**
fun error(): PatchResultError? { * A result of executing a [Patch].
if (this is PatchResultError) { *
return this * @param patchName The name of the [Patch].
} * @param exception The [PatchException] thrown, if any.
return null */
} @Suppress("MemberVisibilityCanBePrivate")
class PatchResult internal constructor(val patchName: String, val exception: PatchException? = null)
fun success(): PatchResultSuccess? {
if (this is PatchResultSuccess) {
return this
}
return null
}
fun isError(): Boolean {
return this is PatchResultError
}
fun isSuccess(): Boolean {
return this is PatchResultSuccess
}
}
class PatchResultError(
errorMessage: String?, cause: Exception?
) : Exception(errorMessage, cause), PatchResult {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Exception) : this(cause.message, cause)
}
class PatchResultSuccess : PatchResult

View File

@@ -16,12 +16,12 @@ annotation class Patch(val include: Boolean = true)
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class DependsOn( annotation class DependsOn(
val dependencies: Array<KClass<out Patch<Context>>> = [] val dependencies: Array<KClass<out Patch<Context<*>>>> = []
) )
// TODO: Remove this annotation, once integrations are coupled with patches.
/** /**
* Annotation to mark [Patch]es which depend on integrations. * Annotation to mark [Patch]es which depend on integrations.
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class RequiresIntegrations // required because integrations are decoupled from patches annotation class RequiresIntegrations

View File

@@ -1,6 +1,6 @@
package app.revanced.patcher.util package app.revanced.patcher.util
import app.revanced.patcher.PatcherContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.logging.Logger import app.revanced.patcher.logging.Logger
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
@@ -8,7 +8,7 @@ import app.revanced.patcher.util.ClassMerger.Utils.filterAny
import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
import app.revanced.patcher.util.ClassMerger.Utils.isPublic import app.revanced.patcher.util.ClassMerger.Utils.isPublic
import app.revanced.patcher.util.ClassMerger.Utils.toPublic import app.revanced.patcher.util.ClassMerger.Utils.toPublic
import app.revanced.patcher.util.TypeUtil.traverseClassHierarchy import app.revanced.patcher.util.ClassMerger.Utils.traverseClassHierarchy
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableField import app.revanced.patcher.util.proxy.mutableTypes.MutableField
@@ -31,8 +31,9 @@ internal object ClassMerger {
* @param otherClass The class to merge with * @param otherClass The class to merge with
* @param context The context to traverse the class hierarchy in. * @param context The context to traverse the class hierarchy in.
* @param logger A logger. * @param logger A logger.
* @return The merged class or the original class if no merge was needed.
*/ */
fun ClassDef.merge(otherClass: ClassDef, context: PatcherContext, logger: Logger? = null) = this fun ClassDef.merge(otherClass: ClassDef, context: BytecodeContext, logger: Logger? = null) = this
//.fixFieldAccess(otherClass, logger) //.fixFieldAccess(otherClass, logger)
//.fixMethodAccess(otherClass, logger) //.fixMethodAccess(otherClass, logger)
.addMissingFields(otherClass, logger) .addMissingFields(otherClass, logger)
@@ -89,10 +90,10 @@ internal object ClassMerger {
* @param context The context to traverse the class hierarchy in. * @param context The context to traverse the class hierarchy in.
* @param logger A logger. * @param logger A logger.
*/ */
private fun ClassDef.publicize(reference: ClassDef, context: PatcherContext, logger: Logger? = null) = private fun ClassDef.publicize(reference: ClassDef, context: BytecodeContext, logger: Logger? = null) =
if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) if (reference.accessFlags.isPublic() && !accessFlags.isPublic())
this.asMutableClass().apply { this.asMutableClass().apply {
context.bytecodeContext.traverseClassHierarchy(this) { context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy if (accessFlags.isPublic()) return@traverseClassHierarchy
logger?.trace("Publicizing ${this.type}") logger?.trace("Publicizing ${this.type}")
@@ -161,6 +162,19 @@ internal object ClassMerger {
} }
private object Utils { private object Utils {
/**
* traverse the class hierarchy starting from the given root class
*
* @param targetClass the class to start traversing the class hierarchy from
* @param callback function that is called for every class in the hierarchy
*/
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
callback(targetClass)
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback)
}
}
fun ClassDef.asMutableClass() = if (this is MutableClass) this else this.toMutable() fun ClassDef.asMutableClass() = if (this is MutableClass) this else this.toMutable()
/** /**

View File

@@ -0,0 +1,87 @@
package app.revanced.patcher.util
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/**
* Wrapper for a file that can be edited as a dom document.
*
* 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.
*
*/
class DomFileEditor internal constructor(
private val inputStream: InputStream,
private val outputStream: Lazy<OutputStream>? = null,
) : Closeable {
// path to the xml file to unlock the resource when closing the editor
private var filePath: String? = null
private var closed: Boolean = false
/**
* The document of the xml file
*/
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
// lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
// increase the lock
locks.merge(file.path, 1, Integer::sum)
filePath = file.path
}
/**
* Closes the editor. Write backs and decreases the lock count.
*
* Will not write back to the file if the file is still locked.
*/
override fun close() {
if (closed) return
inputStream.close()
// if the output stream is not null, do not close it
outputStream?.let {
// prevent writing to same file, if it is being locked
// isLocked will be false if the editor was created through a stream
val isLocked = filePath?.let { path ->
val isLocked = locks[path]!! > 1
// decrease the lock count if the editor was opened for a file
locks.merge(path, -1, Integer::sum)
isLocked
} ?: false
// if unlocked, write back to the file
if (!isLocked) {
it.value.use { stream ->
val result = StreamResult(stream)
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
}
it.value.close()
return
}
}
closed = true
}
private companion object {
// map of concurrent open files
val locks = mutableMapOf<String, Int>()
}
}

View File

@@ -1,15 +0,0 @@
package app.revanced.patcher.util
internal class ListBackedSet<E>(private val list: MutableList<E>) : MutableSet<E> {
override val size get() = list.size
override fun add(element: E) = list.add(element)
override fun addAll(elements: Collection<E>) = list.addAll(elements)
override fun clear() = list.clear()
override fun iterator() = list.listIterator()
override fun remove(element: E) = list.remove(element)
override fun removeAll(elements: Collection<E>) = list.removeAll(elements)
override fun retainAll(elements: Collection<E>) = list.retainAll(elements)
override fun contains(element: E) = list.contains(element)
override fun containsAll(elements: Collection<E>) = list.containsAll(elements)
override fun isEmpty() = list.isEmpty()
}

View File

@@ -1,46 +0,0 @@
package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.iface.ClassDef
/**
* 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>()
/**
* Add a [ClassDef].
*/
fun add(classDef: ClassDef) = classes.add(classDef)
/**
* 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() = 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()
}

View File

@@ -0,0 +1,33 @@
package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.iface.ClassDef
/**
* A class that represents a set of classes and proxies.
*
* @param classes The classes to be backed by proxies.
*/
class ProxyClassList internal constructor(classes: MutableSet<ClassDef>) : MutableSet<ClassDef> by classes {
internal val proxies = mutableListOf<ClassProxy>()
/**
* 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, return false to keep it in the proxies list.
if (!proxy.resolved) return@removeIf false
// If it has been used, replace the original class with the mutable class.
remove(proxy.immutableClass)
add(proxy.mutableClass)
// Return true to remove the proxy from the proxies list.
return@removeIf true
}
}

View File

@@ -1,19 +0,0 @@
package app.revanced.patcher.util
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
object TypeUtil {
/**
* traverse the class hierarchy starting from the given root class
*
* @param targetClass the class to start traversing the class hierarchy from
* @param callback function that is called for every class in the hierarchy
*/
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
callback(targetClass)
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback)
}
}
}

View File

@@ -1,10 +0,0 @@
package app.revanced.patcher.util.dex
import java.io.InputStream
/**
* Wrapper for dex files.
* @param name The original name of the dex file.
* @param stream The dex file as [InputStream].
*/
data class DexFile(val name: String, val stream: InputStream)

View File

@@ -1,76 +0,0 @@
@file:Suppress("unused")
package app.revanced.patcher.util.patch
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import com.android.tools.smali.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.
*/
sealed class PatchBundle(path: String) : File(path) {
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
classNames.forEach { className ->
val clazz = classLoader.loadClass(className)
// Annotations can not Patch.
if (clazz.isAnnotation) return@forEach
clazz.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
?: return@forEach
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Context>>)
}
}.sortedBy { it.patchName }
/**
* 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
),
JarFile(this)
.stream()
.filter { it.name.endsWith(".class") && !it.name.contains("$") }
.map { it.realName.replace('/', '.').replace(".class", "") }.iterator()
)
}
/**
* 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,
DexFileFactory.loadDexFile(path, null).classes.asSequence().map { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
}.iterator()
)
}
}

View File

@@ -2,19 +2,19 @@ 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.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.PatchOption
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.usage.resource.annotation.ExampleResourceCompatibility import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch 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.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Format import com.android.tools.smali.dexlib2.Format
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@@ -29,17 +29,17 @@ import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReferen
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
import com.android.tools.smali.dexlib2.util.Preconditions import com.android.tools.smali.dexlib2.util.Preconditions
import com.google.common.collect.ImmutableList
@Patch @Patch
@Name("example-bytecode-patch") @Name("example-bytecode-patch")
@Description("Example demonstration of a bytecode patch.") @Description("Example demonstration of a bytecode patch.")
@ExampleResourceCompatibility @ExampleResourceCompatibility
@Version("0.0.1")
@DependsOn([ExampleResourcePatch::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(context: BytecodeContext): PatchResult { override fun execute(context: BytecodeContext) {
// 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!!
@@ -126,12 +126,6 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
""" """
) )
// Finally, tell the patcher that this patch was a success.
// You can also return PatchResultError with a message.
// If an exception is thrown inside this function,
// a PatchResultError will be returned with the error message.
return PatchResultSuccess()
} }
/** /**

Some files were not shown because too many files have changed in this diff Show More