mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 01:02:22 +01:00
chore: merge branch dev
to main
(#217)
This commit is contained in:
commit
30bd4fd9fe
33 changed files with 842 additions and 804 deletions
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -1,3 +1,41 @@
|
||||||
|
# [14.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0-dev.3...v14.0.0-dev.4) (2023-08-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* only emit closed patches that did not throw an exception with the `@Patch` annotation ([5938f6b](https://github.com/ReVanced/revanced-patcher/commit/5938f6b7ea25103a0a1b56ceebe49139bc80c6f5))
|
||||||
|
|
||||||
|
# [14.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0-dev.2...v14.0.0-dev.3) (2023-08-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* supply the parent classloader to `DexClassLoader` ([0f15077](https://github.com/ReVanced/revanced-patcher/commit/0f15077225600b65200022c1a318e504deb472b9))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* do not log instantiation of ReVanced Patcher ([273dd8d](https://github.com/ReVanced/revanced-patcher/commit/273dd8d388f8e9b7436c6d6145a94c12c1fabe55))
|
||||||
|
|
||||||
|
# [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)
|
# [13.0.0](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1...v13.0.0) (2023-08-14)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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-5")
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
version = 14.0.0-dev.4
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface IntegrationsConsumer {
|
||||||
|
fun acceptIntegrations(integrations: List<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
|
|
||||||
}
|
}
|
71
src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt
Normal file
71
src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
@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.DexClassLoader
|
||||||
|
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(vararg 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 { it.name.replace('/', '.').replace(".class", "") }
|
||||||
|
.map { loadClass(it) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchBundleLoader] for [Dex] files.
|
||||||
|
*
|
||||||
|
* @param patchBundles The path to patch bundles of DEX format.
|
||||||
|
*/
|
||||||
|
class Dex(vararg patchBundles: File) : PatchBundleLoader(with(
|
||||||
|
DexClassLoader(
|
||||||
|
patchBundles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
PatchBundleLoader::class.java.classLoader
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
patchBundles
|
||||||
|
.flatMap {
|
||||||
|
MultiDexIO.readDexFile(true, it, BasicDexFileNamer(), null, null).classes
|
||||||
|
}
|
||||||
|
.map { classDef -> classDef.type.substring(1, classDef.length - 1) }
|
||||||
|
.map { loadClass(it) }
|
||||||
|
})
|
||||||
|
}
|
|
@ -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>>
|
|
@ -1,64 +1,41 @@
|
||||||
package app.revanced.patcher
|
package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.data.Context
|
import app.revanced.patcher.data.Context
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
|
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
|
||||||
import app.revanced.patcher.patch.*
|
import app.revanced.patcher.patch.*
|
||||||
import brut.androlib.AaptInvoker
|
import kotlinx.coroutines.flow.flow
|
||||||
import brut.androlib.ApkDecoder
|
|
||||||
import brut.androlib.Config
|
|
||||||
import brut.androlib.apk.ApkInfo
|
|
||||||
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 com.android.tools.smali.dexlib2.Opcodes
|
|
||||||
import com.android.tools.smali.dexlib2.iface.DexFile
|
|
||||||
import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore
|
|
||||||
import lanchon.multidexlib2.BasicDexFileNamer
|
|
||||||
import lanchon.multidexlib2.DexIO
|
|
||||||
import lanchon.multidexlib2.MultiDexIO
|
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.OutputStream
|
import java.util.function.Supplier
|
||||||
import java.nio.file.Files
|
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
import java.util.logging.LogManager
|
import java.util.logging.LogManager
|
||||||
|
|
||||||
internal val NAMER = BasicDexFileNamer()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ReVanced Patcher.
|
* ReVanced Patcher.
|
||||||
|
*
|
||||||
* @param options The options for the patcher.
|
* @param options The options for the patcher.
|
||||||
*/
|
*/
|
||||||
class Patcher(private val options: PatcherOptions) {
|
class Patcher(
|
||||||
val context: PatcherContext
|
private val options: PatcherOptions
|
||||||
|
) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable {
|
||||||
private val logger = options.logger
|
/**
|
||||||
|
* The context of ReVanced [Patcher].
|
||||||
private val opcodes: Opcodes
|
* This holds the current state of the patcher.
|
||||||
|
*/
|
||||||
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
|
val context = PatcherContext(options)
|
||||||
|
|
||||||
private var mergeIntegrations = false
|
|
||||||
|
|
||||||
private val config = Config.getDefaultConfig().apply {
|
|
||||||
useAapt2 = true
|
|
||||||
aaptPath = options.aaptPath
|
|
||||||
frameworkDirectory = options.frameworkDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Disable unwanted logging.
|
|
||||||
LogManager.getLogManager().let { manager ->
|
LogManager.getLogManager().let { manager ->
|
||||||
manager.getLogger("").level = Level.OFF // Disable root logger.
|
// Disable root logger.
|
||||||
// Enable only ReVanced logging.
|
manager.getLogger("").level = Level.OFF
|
||||||
|
|
||||||
|
// Enable ReVanced logging only.
|
||||||
manager.loggerNames
|
manager.loggerNames
|
||||||
.toList()
|
.toList()
|
||||||
.filter { it.startsWith("app.revanced") }
|
.filter { it.startsWith("app.revanced") }
|
||||||
|
@ -66,117 +43,22 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
.forEach { it.level = Level.INFO }
|
.forEach { it.level = Level.INFO }
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Reading dex files")
|
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY)
|
||||||
|
|
||||||
// read dex files
|
|
||||||
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
|
||||||
|
|
||||||
// get the opcodes
|
|
||||||
opcodes = dexFile.opcodes
|
|
||||||
|
|
||||||
// finally create patcher context
|
|
||||||
context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
|
|
||||||
|
|
||||||
// decode manifest file
|
|
||||||
decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun acceptPatches(patches: List<PatchClass>) {
|
||||||
* Add integrations to be merged by the patcher.
|
|
||||||
* The integrations will only be merged, if necessary.
|
|
||||||
*
|
|
||||||
* @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files.
|
|
||||||
* @param callback The callback for [integrations] which are being added.
|
|
||||||
*/
|
|
||||||
fun addIntegrations(
|
|
||||||
integrations: List<File>,
|
|
||||||
callback: (File) -> Unit
|
|
||||||
) {
|
|
||||||
context.integrations.apply integrations@{
|
|
||||||
add(integrations)
|
|
||||||
this@integrations.callback = callback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the patched dex file.
|
|
||||||
*/
|
|
||||||
fun save(): PatcherResult {
|
|
||||||
var resourceFile: File? = null
|
|
||||||
|
|
||||||
if (resourceDecodingMode == ResourceDecodingMode.FULL) {
|
|
||||||
logger.info("Compiling resources")
|
|
||||||
|
|
||||||
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
|
|
||||||
val aaptFile = cacheDirectory.resolve("aapt_temp_file").also {
|
|
||||||
Files.deleteIfExists(it.toPath())
|
|
||||||
}.also { resourceFile = it }
|
|
||||||
|
|
||||||
try {
|
|
||||||
AaptInvoker(
|
|
||||||
config,
|
|
||||||
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(config).getFrameworkApk(id, usesFramework.tag)
|
|
||||||
}.toTypedArray()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
cacheDirectory.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Writing modified dex files")
|
|
||||||
|
|
||||||
return mutableMapOf<String, MemoryDataStore>().apply {
|
|
||||||
MultiDexIO.writeDexFile(
|
|
||||||
true,
|
|
||||||
-1, // Defaults to amount of available cores.
|
|
||||||
this,
|
|
||||||
NAMER,
|
|
||||||
object : DexFile {
|
|
||||||
override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
|
|
||||||
override fun getOpcodes() = this@Patcher.opcodes
|
|
||||||
},
|
|
||||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}.let { dexFiles ->
|
|
||||||
PatcherResult(
|
|
||||||
dexFiles.map {
|
|
||||||
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
|
|
||||||
},
|
|
||||||
context.packageMetadata.apkInfo.doNotCompress?.toList(),
|
|
||||||
resourceFile
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add [Patch]es to the patcher.
|
|
||||||
* @param patches [Patch]es The patches to add.
|
|
||||||
*/
|
|
||||||
fun addPatches(patches: Iterable<Class<out Patch<Context>>>) {
|
|
||||||
/**
|
/**
|
||||||
* Returns true if at least one patches or its dependencies matches the given predicate.
|
* Returns true if at least one patches or its dependencies matches the given predicate.
|
||||||
*/
|
*/
|
||||||
fun Class<out Patch<Context>>.anyRecursively(predicate: (Class<out Patch<Context>>) -> Boolean): Boolean =
|
fun PatchClass.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean =
|
||||||
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
|
predicate(this) || dependencies?.any { dependency ->
|
||||||
|
dependency.java.anyRecursively(predicate)
|
||||||
|
} ?: false
|
||||||
|
|
||||||
// Determine if resource patching is required.
|
// Determine if resource patching is required.
|
||||||
for (patch in patches) {
|
for (patch in patches) {
|
||||||
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
|
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
|
||||||
resourceDecodingMode = ResourceDecodingMode.FULL
|
options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,7 +66,7 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
// Determine if merging integrations is required.
|
// Determine if merging integrations is required.
|
||||||
for (patch in patches) {
|
for (patch in patches) {
|
||||||
if (patch.anyRecursively { it.requiresIntegrations }) {
|
if (patch.anyRecursively { it.requiresIntegrations }) {
|
||||||
mergeIntegrations = true
|
context.bytecodeContext.integrations.merge = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,209 +75,159 @@ class Patcher(private val options: PatcherOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode resources for the patcher.
|
* Add integrations to the [Patcher].
|
||||||
*
|
*
|
||||||
* @param mode The [ResourceDecodingMode] to use when decoding.
|
* @param integrations The integrations to add. Must be a DEX file or container of DEX files.
|
||||||
*/
|
*/
|
||||||
private fun decodeResources(mode: ResourceDecodingMode) {
|
override fun acceptIntegrations(integrations: List<File>) {
|
||||||
val apkInfo = ApkInfo(ExtFile(options.inputFile)).also { context.packageMetadata.apkInfo = it }
|
context.bytecodeContext.integrations.addAll(integrations)
|
||||||
|
|
||||||
// Needed to record uncompressed files.
|
|
||||||
val apkDecoder = ApkDecoder(config, apkInfo)
|
|
||||||
|
|
||||||
// Needed to decode resources.
|
|
||||||
val resourcesDecoder = ResourcesDecoder(config, apkInfo)
|
|
||||||
|
|
||||||
try {
|
|
||||||
when (mode) {
|
|
||||||
ResourceDecodingMode.FULL -> {
|
|
||||||
logger.info("Decoding resources")
|
|
||||||
|
|
||||||
val outDir = options.recreateResourceCacheDirectory()
|
|
||||||
|
|
||||||
resourcesDecoder.decodeResources(outDir)
|
|
||||||
resourcesDecoder.decodeManifest(outDir)
|
|
||||||
|
|
||||||
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
|
|
||||||
|
|
||||||
apkInfo.usesFramework = UsesFramework().apply {
|
|
||||||
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ResourceDecodingMode.MANIFEST_ONLY -> {
|
|
||||||
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(
|
|
||||||
apkInfo.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
|
|
||||||
apkInfo.versionInfo.let {
|
|
||||||
metadata.packageVersion = it.versionName ?: it.versionCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
apkInfo.apkFile.close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute patches added the patcher.
|
* Execute [Patch]es that were added to ReVanced [Patcher].
|
||||||
*
|
*
|
||||||
* @param stopOnError If true, the patches will stop on the first error.
|
* @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails.
|
||||||
* @return A pair of the name of the [Patch] and its [PatchResult].
|
* @return A pair of the name of the [Patch] and its [PatchResult].
|
||||||
*/
|
*/
|
||||||
fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
|
override fun apply(returnOnError: Boolean) = flow {
|
||||||
|
class ExecutedPatch(val patchInstance: Patch<Context<*>>, val patchResult: PatchResult)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a [Patch] and its dependencies recursively.
|
* Execute a [Patch] and its dependencies recursively.
|
||||||
*
|
*
|
||||||
* @param patchClass The [Patch] to execute.
|
* @param patchClass The [Patch] to execute.
|
||||||
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
|
* @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
|
||||||
* @return The result of executing the [Patch].
|
* @return The result of executing the [Patch].
|
||||||
*/
|
*/
|
||||||
fun executePatch(
|
fun executePatch(
|
||||||
patchClass: Class<out Patch<Context>>,
|
patchClass: PatchClass,
|
||||||
executedPatches: LinkedHashMap<String, ExecutedPatch>
|
executedPatches: LinkedHashMap<String, ExecutedPatch>
|
||||||
): PatchResult {
|
): PatchResult {
|
||||||
val patchName = patchClass.patchName
|
val patchName = patchClass.patchName
|
||||||
|
|
||||||
// if the patch has already applied silently skip it
|
executedPatches[patchName]?.let { executedPatch ->
|
||||||
if (executedPatches.contains(patchName)) {
|
executedPatch.patchResult.exception ?: return executedPatch.patchResult
|
||||||
if (!executedPatches[patchName]!!.success)
|
|
||||||
return PatchResultError("'$patchName' did not succeed previously")
|
|
||||||
|
|
||||||
logger.trace("Skipping '$patchName' because it has already been applied")
|
// Return a new result with an exception indicating that the patch was not executed previously,
|
||||||
|
// because it is a dependency of another patch that failed.
|
||||||
return PatchResultSuccess()
|
return PatchResult(patchName, PatchException("'$patchName' did not succeed previously"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// recursively execute all dependency patches
|
// Recursively execute all dependency patches.
|
||||||
patchClass.dependencies?.forEach { dependencyClass ->
|
patchClass.dependencies?.forEach { dependencyClass ->
|
||||||
val dependency = dependencyClass.java
|
val dependency = dependencyClass.java
|
||||||
|
|
||||||
val result = executePatch(dependency, executedPatches)
|
val result = executePatch(dependency, executedPatches)
|
||||||
if (result.isSuccess()) return@forEach
|
|
||||||
|
|
||||||
return PatchResultError(
|
result.exception?.let {
|
||||||
"'$patchName' depends on '${dependency.patchName}' but the following error was raised: " +
|
return PatchResult(
|
||||||
result.error()!!.let { it.cause?.stackTraceToString() ?: it.message }
|
patchName,
|
||||||
)
|
PatchException(
|
||||||
|
"'$patchName' depends on '${dependency.patchName}' that raised an exception: $it"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
|
// TODO: Implement this in a more polymorphic way.
|
||||||
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
|
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
|
||||||
|
|
||||||
// TODO: implement this in a more polymorphic way
|
val patchContext = if (patchInstance is BytecodePatch) {
|
||||||
val patchContext = if (isResourcePatch) {
|
patchInstance.fingerprints?.resolveUsingLookupMap(context.bytecodeContext)
|
||||||
context.resourceContext
|
|
||||||
} else {
|
|
||||||
context.bytecodeContext.also { context ->
|
|
||||||
(patchInstance as BytecodePatch).fingerprints?.resolveUsingLookupMap(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
|
context.bytecodeContext
|
||||||
|
} else {
|
||||||
|
context.resourceContext
|
||||||
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
patchInstance.execute(patchContext).also {
|
patchInstance.execute(patchContext)
|
||||||
executedPatches[patchName] = ExecutedPatch(patchInstance, it.isSuccess())
|
|
||||||
}
|
PatchResult(patchName)
|
||||||
} catch (e: Exception) {
|
} catch (exception: PatchException) {
|
||||||
PatchResultError(e).also {
|
PatchResult(patchName, exception)
|
||||||
executedPatches[patchName] = ExecutedPatch(patchInstance, false)
|
} catch (exception: Exception) {
|
||||||
}
|
PatchResult(patchName, PatchException(exception))
|
||||||
|
}.also { executedPatches[patchName] = ExecutedPatch(patchInstance, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
|
||||||
|
|
||||||
|
MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext)
|
||||||
|
|
||||||
|
// Prevent from decoding the app manifest twice if it is not needed.
|
||||||
|
if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL)
|
||||||
|
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL)
|
||||||
|
|
||||||
|
options.logger.info("Executing patches")
|
||||||
|
|
||||||
|
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // Key is name.
|
||||||
|
|
||||||
|
context.patches.forEach { patch ->
|
||||||
|
val result = executePatch(patch, executedPatches)
|
||||||
|
|
||||||
|
// If the patch failed, or if the patch is not closeable, emit the result.
|
||||||
|
// Results of patches that are closeable will be emitted later.
|
||||||
|
result.exception?.let {
|
||||||
|
emit(result)
|
||||||
|
|
||||||
|
if (returnOnError) return@flow
|
||||||
|
} ?: run {
|
||||||
|
if (executedPatches[result.patchName]!!.patchInstance is Closeable) return@run
|
||||||
|
|
||||||
|
emit(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sequence {
|
executedPatches.values
|
||||||
if (mergeIntegrations) context.integrations.merge(logger)
|
.filter { it.patchResult.exception == null }
|
||||||
|
.filter { it.patchInstance is Closeable }.asReversed().forEach { executedPatch ->
|
||||||
|
val patchName = executedPatch.patchResult.patchName
|
||||||
|
|
||||||
logger.trace("Initialize lookup maps for method MethodFingerprint resolution")
|
val result = try {
|
||||||
|
(executedPatch.patchInstance as Closeable).close()
|
||||||
|
|
||||||
MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext)
|
executedPatch.patchResult
|
||||||
|
} catch (exception: PatchException) {
|
||||||
// prevent from decoding the manifest twice if it is not needed
|
PatchResult(patchName, exception)
|
||||||
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
|
} catch (exception: Exception) {
|
||||||
|
PatchResult(patchName, PatchException(exception))
|
||||||
logger.info("Executing patches")
|
|
||||||
|
|
||||||
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // first is name
|
|
||||||
|
|
||||||
context.patches.forEach { patch ->
|
|
||||||
val patchResult = executePatch(patch, executedPatches)
|
|
||||||
|
|
||||||
val result = if (patchResult.isSuccess()) {
|
|
||||||
Result.success(patchResult.success()!!)
|
|
||||||
} else {
|
|
||||||
Result.failure(patchResult.error()!!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This prints before the patch really finishes in case it is a Closeable
|
result.exception?.let {
|
||||||
// because the Closeable is closed after all patches are executed.
|
emit(
|
||||||
yield(patch.patchName to result)
|
PatchResult(
|
||||||
|
patchName,
|
||||||
|
PatchException("'$patchName' raised an exception while being closed: $it")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (stopOnError && patchResult.isError()) return@sequence
|
if (returnOnError) return@flow
|
||||||
|
} ?: run {
|
||||||
|
executedPatch
|
||||||
|
.patchInstance::class
|
||||||
|
.java
|
||||||
|
.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
|
||||||
|
?: return@run
|
||||||
|
|
||||||
|
emit(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
executedPatches.values
|
override fun close() {
|
||||||
.filter(ExecutedPatch::success)
|
MethodFingerprint.clearFingerprintResolutionLookupMaps()
|
||||||
.map(ExecutedPatch::patchInstance)
|
|
||||||
.filterIsInstance(Closeable::class.java)
|
|
||||||
.asReversed().forEach {
|
|
||||||
try {
|
|
||||||
it.close()
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
val patchName = (it as Patch<Context>).javaClass.patchName
|
|
||||||
|
|
||||||
logger.error("Failed to close '$patchName': ${exception.stackTraceToString()}")
|
|
||||||
|
|
||||||
yield(patchName to Result.failure(exception))
|
|
||||||
|
|
||||||
// This is not failsafe. If a patch throws an exception while closing,
|
|
||||||
// the other patches that depend on it may fail.
|
|
||||||
if (stopOnError) return@sequence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodFingerprint.clearFingerprintResolutionLookupMaps()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of decoding the resources.
|
* Compile and save the patched APK file.
|
||||||
|
*
|
||||||
|
* @return The [PatcherResult] containing the patched input files.
|
||||||
*/
|
*/
|
||||||
private enum class ResourceDecodingMode {
|
override fun get() = PatcherResult(
|
||||||
/**
|
context.bytecodeContext.get(),
|
||||||
* Decode all resources.
|
context.resourceContext.get(),
|
||||||
*/
|
context.packageMetadata.apkInfo.doNotCompress?.toList()
|
||||||
FULL,
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode the manifest file only.
|
|
||||||
*/
|
|
||||||
MANIFEST_ONLY,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)
|
|
||||||
|
|
|
@ -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>,
|
* A context for ReVanced [Patcher].
|
||||||
val resourceCacheDirectory: File,
|
*
|
||||||
) {
|
* @param options The [PatcherOptions] used to create this context.
|
||||||
val packageMetadata = PackageMetadata()
|
*/
|
||||||
internal val patches = mutableListOf<Class<out Patch<Context>>>()
|
class PatcherContext internal constructor(options: PatcherOptions) {
|
||||||
internal val integrations = Integrations(this)
|
/**
|
||||||
internal val bytecodeContext = BytecodeContext(classes)
|
* [PackageMetadata] of the supplied [PatcherOptions.inputFile].
|
||||||
internal val resourceContext = ResourceContext(resourceCacheDirectory)
|
*/
|
||||||
|
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile)))
|
||||||
|
|
||||||
internal class Integrations(val context: PatcherContext) {
|
/**
|
||||||
var callback: ((File) -> Unit)? = null
|
* The list of [Patch]es to execute.
|
||||||
private val integrations: MutableList<File> = mutableListOf()
|
*/
|
||||||
|
internal val patches = mutableListOf<PatchClass>()
|
||||||
|
|
||||||
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
|
/**
|
||||||
|
* The [ResourceContext] of this [PatcherContext].
|
||||||
|
* This holds the current state of the resources.
|
||||||
|
*/
|
||||||
|
internal val resourceContext = ResourceContext(this, options)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge integrations.
|
* The [BytecodeContext] of this [PatcherContext].
|
||||||
* @param logger A logger.
|
* This holds the current state of the bytecode.
|
||||||
*/
|
*/
|
||||||
fun merge(logger: Logger) {
|
internal val bytecodeContext = BytecodeContext(options)
|
||||||
with(context.bytecodeContext.classes) {
|
|
||||||
for (integrations in integrations) {
|
|
||||||
callback?.let { it(integrations) }
|
|
||||||
|
|
||||||
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile(
|
|
||||||
true,
|
|
||||||
integrations,
|
|
||||||
NAMER,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
).classes) {
|
|
||||||
val type = classDef.type
|
|
||||||
|
|
||||||
val result = classes.findIndexed { it.type == type }
|
|
||||||
if (result == null) {
|
|
||||||
logger.trace("Merging type $type")
|
|
||||||
classes.add(classDef)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val (existingClass, existingClassIndex) = result
|
|
||||||
|
|
||||||
logger.trace("Type $type exists. Adding missing methods and fields.")
|
|
||||||
|
|
||||||
existingClass.merge(classDef, context, logger).let { mergedClass ->
|
|
||||||
if (mergedClass !== existingClass) // referential equality check
|
|
||||||
classes[existingClassIndex] = mergedClass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
8
src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt
Normal file
8
src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.PatchClass
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface PatchesConsumer {
|
||||||
|
fun acceptPatches(patches: List<PatchClass>)
|
||||||
|
}
|
|
@ -18,15 +18,4 @@ annotation class Name(
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
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,
|
|
||||||
)
|
|
152
src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt
Normal file
152
src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt
Normal 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)) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>()
|
|
||||||
}
|
|
||||||
}
|
|
157
src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt
Normal file
157
src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt
Normal 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])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
12
src/main/kotlin/app/revanced/patcher/patch/PatchException.kt
Normal file
12
src/main/kotlin/app/revanced/patcher/patch/PatchException.kt
Normal 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)
|
||||||
|
}
|
|
@ -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
|
|
|
@ -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
|
|
@ -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()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
87
src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt
Normal file
87
src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt
Normal 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>()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
33
src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt
Normal file
33
src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
|
@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,10 +2,7 @@ package app.revanced.patcher.usage.resource.patch
|
||||||
|
|
||||||
import app.revanced.patcher.annotation.Description
|
import app.revanced.patcher.annotation.Description
|
||||||
import app.revanced.patcher.annotation.Name
|
import app.revanced.patcher.annotation.Name
|
||||||
import app.revanced.patcher.annotation.Version
|
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.data.ResourceContext
|
||||||
import app.revanced.patcher.patch.PatchResult
|
|
||||||
import app.revanced.patcher.patch.PatchResultSuccess
|
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
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
|
||||||
|
@ -15,9 +12,8 @@ import org.w3c.dom.Element
|
||||||
@Name("example-resource-patch")
|
@Name("example-resource-patch")
|
||||||
@Description("Example demonstration of a resource patch.")
|
@Description("Example demonstration of a resource patch.")
|
||||||
@ExampleResourceCompatibility
|
@ExampleResourceCompatibility
|
||||||
@Version("0.0.1")
|
|
||||||
class ExampleResourcePatch : ResourcePatch {
|
class ExampleResourcePatch : ResourcePatch {
|
||||||
override fun execute(context: ResourceContext): PatchResult {
|
override fun execute(context: ResourceContext) {
|
||||||
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
||||||
val element = editor // regular DomFileEditor
|
val element = editor // regular DomFileEditor
|
||||||
.file
|
.file
|
||||||
|
@ -29,7 +25,5 @@ class ExampleResourcePatch : ResourcePatch {
|
||||||
"exampleValue"
|
"exampleValue"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PatchResultSuccess()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue