chore: merge branch dev to main (#217)

This commit is contained in:
oSumAtrIX 2023-08-22 19:15:09 +02:00 committed by GitHub
commit 30bd4fd9fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 842 additions and 804 deletions

View file

@ -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)

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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) }
})
}

View file

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

View file

@ -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)

View file

@ -1,64 +1,37 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.* import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.logging.Logger import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.patch.PatchClass
import com.android.tools.smali.dexlib2.iface.ClassDef import brut.androlib.apk.ApkInfo
import java.io.File import brut.directory.ExtFile
data class PatcherContext( /**
val classes: MutableList<ClassDef>, * 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
}
}
}
}
}
}
} }

View file

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

View file

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

View file

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

View file

@ -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,
)

View file

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

View file

@ -1,170 +1,9 @@
package app.revanced.patcher.data package app.revanced.patcher.data
import app.revanced.patcher.util.ProxyBackedClassList import java.util.function.Supplier
import app.revanced.patcher.util.method.MethodWalker
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/** /**
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext]. * A common interface for contexts such as [ResourceContext] and [BytecodeContext].
*/ */
sealed interface Context sealed interface Context<T> : Supplier<T>
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
/**
* The list of classes.
*/
val classes = ProxyBackedClassList(classes)
/**
* Find a class by a given class name.
*
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.add(proxy)
}
return proxy
}
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
return MethodWalker(this, startMethod)
}
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
for ((index, element) in this.withIndex()) {
if (predicate(element)) {
return element to index
}
}
return null
}
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
val xmlEditor = XmlFileHolder()
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
inner class XmlFileHolder {
operator fun get(inputStream: InputStream) =
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path])
}
}
}
/**
* Wrapper for a file that can be edited as a dom document.
*
* This constructor does not check for locks to the file when writing.
* Use the secondary constructor.
*
* @param inputStream the input stream to read the xml file from.
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
*
*/
class DomFileEditor internal constructor(
private val inputStream: InputStream,
private val outputStream: Lazy<OutputStream>? = null,
) : Closeable {
// path to the xml file to unlock the resource when closing the editor
private var filePath: String? = null
private var closed: Boolean = false
/**
* The document of the xml file
*/
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
// lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
// increase the lock
locks.merge(file.path, 1, Integer::sum)
filePath = file.path
}
/**
* Closes the editor. Write backs and decreases the lock count.
*
* Will not write back to the file if the file is still locked.
*/
override fun close() {
if (closed) return
inputStream.close()
// if the output stream is not null, do not close it
outputStream?.let {
// prevent writing to same file, if it is being locked
// isLocked will be false if the editor was created through a stream
val isLocked = filePath?.let { path ->
val isLocked = locks[path]!! > 1
// decrease the lock count if the editor was opened for a file
locks.merge(path, -1, Integer::sum)
isLocked
} ?: false
// if unlocked, write back to the file
if (!isLocked) {
it.value.use { stream ->
val result = StreamResult(stream)
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
}
it.value.close()
return
}
}
closed = true
}
private companion object {
// map of concurrent open files
val locks = mutableMapOf<String, Int>()
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -6,20 +6,22 @@ import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import java.io.Closeable import java.io.Closeable
typealias PatchClass = Class<out Patch<Context<*>>>
/** /**
* A ReVanced patch. * A ReVanced patch.
* *
* If it implements [Closeable], it will be closed after all patches have been executed. * If it implements [Closeable], it will be closed after all patches have been executed.
* Closing will be done in reverse execution order. * Closing will be done in reverse execution order.
*/ */
sealed interface Patch<out T : Context> { sealed interface Patch<out T : Context<*>> {
/** /**
* The main function of the [Patch] which the patcher will call. * The main function of the [Patch] which the patcher will call.
* *
* @param context The [Context] the patch will work on. * @param context The [Context] the patch will work on.
* @return The result of executing the patch. * @return The result of executing the patch.
*/ */
fun execute(context: @UnsafeVariance T): PatchResult fun execute(context: @UnsafeVariance T)
} }
/** /**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,46 +0,0 @@
package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.iface.ClassDef
/**
* A class that represents a set of classes and proxies.
*
* @param classes The classes to be backed by proxies.
*/
class ProxyBackedClassList(internal val classes: MutableList<ClassDef>) : Set<ClassDef> {
internal val proxies = mutableListOf<ClassProxy>()
/**
* Add a [ClassDef].
*/
fun add(classDef: ClassDef) = classes.add(classDef)
/**
* Add a [ClassProxy].
*/
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
/**
* Replace all classes with their mutated versions.
*/
internal fun replaceClasses() =
proxies.removeIf { proxy ->
// if the proxy is unused, keep it in the list
if (!proxy.resolved) return@removeIf false
// if it has been used, replace the original class with the new class
val index = classes.indexOfFirst { it.type == proxy.immutableClass.type }
classes[index] = proxy.mutableClass
// return true to remove it from the proxies list
return@removeIf true
}
override val size get() = classes.size
override fun contains(element: ClassDef) = classes.contains(element)
override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
override fun isEmpty() = classes.isEmpty()
override fun iterator() = classes.iterator()
}

View file

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

View file

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

View file

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

View file

@ -1,76 +0,0 @@
@file:Suppress("unused")
package app.revanced.patcher.util.patch
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import com.android.tools.smali.dexlib2.DexFileFactory
import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
/**
* A patch bundle.
* @param path The path to the patch bundle.
*/
sealed class PatchBundle(path: String) : File(path) {
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
classNames.forEach { className ->
val clazz = classLoader.loadClass(className)
// Annotations can not Patch.
if (clazz.isAnnotation) return@forEach
clazz.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
?: return@forEach
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Context>>)
}
}.sortedBy { it.patchName }
/**
* A patch bundle of type [Jar].
*
* @param patchBundlePath The path to the patch bundle.
*/
class Jar(patchBundlePath: String) : PatchBundle(patchBundlePath) {
/**
* Load patches from the patch bundle.
*
* Patches will be loaded with a new [URLClassLoader].
*/
fun loadPatches() = loadPatches(
URLClassLoader(
arrayOf(this.toURI().toURL()),
Thread.currentThread().contextClassLoader // TODO: find out why this is required
),
JarFile(this)
.stream()
.filter { it.name.endsWith(".class") && !it.name.contains("$") }
.map { it.realName.replace('/', '.').replace(".class", "") }.iterator()
)
}
/**
* A patch bundle of type [Dex] format.
*
* @param patchBundlePath The path to a patch bundle of dex format.
* @param dexClassLoader The dex class loader.
*/
class Dex(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) {
/**
* Load patches from the patch bundle.
*
* Patches will be loaded to the provided [dexClassLoader].
*/
fun loadPatches() = loadPatches(dexClassLoader,
DexFileFactory.loadDexFile(path, null).classes.asSequence().map { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
}.iterator()
)
}
}

View file

@ -2,19 +2,19 @@ package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.google.common.collect.ImmutableList
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Format import com.android.tools.smali.dexlib2.Format
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@ -29,17 +29,17 @@ import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReferen
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
import com.android.tools.smali.dexlib2.util.Preconditions import com.android.tools.smali.dexlib2.util.Preconditions
import com.google.common.collect.ImmutableList
@Patch @Patch
@Name("example-bytecode-patch") @Name("example-bytecode-patch")
@Description("Example demonstration of a bytecode patch.") @Description("Example demonstration of a bytecode patch.")
@ExampleResourceCompatibility @ExampleResourceCompatibility
@Version("0.0.1")
@DependsOn([ExampleResourcePatch::class]) @DependsOn([ExampleResourcePatch::class])
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// This function will be executed by the patcher. // This function will be executed by the patcher.
// You can treat it as a constructor // You can treat it as a constructor
override fun execute(context: BytecodeContext): PatchResult { override fun execute(context: BytecodeContext) {
// Get the resolved method by its fingerprint from the resolver cache // Get the resolved method by its fingerprint from the resolver cache
val result = ExampleFingerprint.result!! val result = ExampleFingerprint.result!!
@ -126,12 +126,6 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
""" """
) )
// Finally, tell the patcher that this patch was a success.
// You can also return PatchResultError with a message.
// If an exception is thrown inside this function,
// a PatchResultError will be returned with the error message.
return PatchResultSuccess()
} }
/** /**

View file

@ -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()
} }
} }