diff --git a/CHANGELOG.md b/CHANGELOG.md index 5670974..43efe5e 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/build.gradle.kts b/build.gradle.kts index 12a864f..39534d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,7 @@ val githubPassword: String = project.findProperty("gpr.key") as? String ?: Syste repositories { mavenCentral() google() + mavenLocal() listOf("multidexlib2", "apktool").forEach { repo -> maven { url = uri("https://maven.pkg.github.com/revanced/$repo") @@ -23,12 +24,15 @@ repositories { } dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("xpp3:xpp3:1.1.4c") implementation("com.android.tools.smali:smali:3.0.3") 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") + + compileOnly("com.google.android:android:4.1.1.4") + testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") } diff --git a/gradle.properties b/gradle.properties index 2265e44..a98d663 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 13.0.0 +version = 14.0.0-dev.4 diff --git a/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt b/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt new file mode 100644 index 0000000..27ab7b9 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt @@ -0,0 +1,8 @@ +package app.revanced.patcher + +import java.io.File + +@FunctionalInterface +interface IntegrationsConsumer { + fun acceptIntegrations(integrations: List) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt b/src/main/kotlin/app/revanced/patcher/PackageMetadata.kt similarity index 64% rename from src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt rename to src/main/kotlin/app/revanced/patcher/PackageMetadata.kt index f6c1886..2a87cad 100644 --- a/src/main/kotlin/app/revanced/patcher/data/PackageMetadata.kt +++ b/src/main/kotlin/app/revanced/patcher/PackageMetadata.kt @@ -1,15 +1,14 @@ -package app.revanced.patcher.data +package app.revanced.patcher import brut.androlib.apk.ApkInfo /** * Metadata about a package. */ -class PackageMetadata { +class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) { lateinit var packageName: String internal set + lateinit var packageVersion: String internal set - - internal lateinit var apkInfo: ApkInfo } diff --git a/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt b/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt new file mode 100644 index 0000000..c240152 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt @@ -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> +) : MutableList 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) } + }) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt b/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt new file mode 100644 index 0000000..6ef5c12 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt @@ -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> \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index ad0d0d3..c2e60af 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,64 +1,41 @@ package app.revanced.patcher 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.patchName import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap import app.revanced.patcher.patch.* -import brut.androlib.AaptInvoker -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 kotlinx.coroutines.flow.flow import java.io.Closeable import java.io.File -import java.io.OutputStream -import java.nio.file.Files +import java.util.function.Supplier import java.util.logging.Level import java.util.logging.LogManager -internal val NAMER = BasicDexFileNamer() - /** - * The ReVanced Patcher. + * ReVanced Patcher. + * * @param options The options for the patcher. */ -class Patcher(private val options: PatcherOptions) { - val context: PatcherContext - - private val logger = options.logger - - private val opcodes: Opcodes - - private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY - - private var mergeIntegrations = false - - private val config = Config.getDefaultConfig().apply { - useAapt2 = true - aaptPath = options.aaptPath - frameworkDirectory = options.frameworkDirectory - } +class Patcher( + private val options: PatcherOptions +) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier, Closeable { + /** + * The context of ReVanced [Patcher]. + * This holds the current state of the patcher. + */ + val context = PatcherContext(options) init { - // Disable unwanted logging. LogManager.getLogManager().let { manager -> - manager.getLogger("").level = Level.OFF // Disable root logger. - // Enable only ReVanced logging. + // Disable root logger. + manager.getLogger("").level = Level.OFF + + // Enable ReVanced logging only. manager.loggerNames .toList() .filter { it.startsWith("app.revanced") } @@ -66,117 +43,22 @@ class Patcher(private val options: PatcherOptions) { .forEach { it.level = Level.INFO } } - logger.info("Reading dex files") - - // 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) + context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY) } - /** - * 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, - 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().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>>) { + override fun acceptPatches(patches: List) { /** * Returns true if at least one patches or its dependencies matches the given predicate. */ - fun Class>.anyRecursively(predicate: (Class>) -> Boolean): Boolean = - predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true - + fun PatchClass.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean = + predicate(this) || dependencies?.any { dependency -> + dependency.java.anyRecursively(predicate) + } ?: false // Determine if resource patching is required. for (patch in patches) { if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) { - resourceDecodingMode = ResourceDecodingMode.FULL + options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL break } } @@ -184,7 +66,7 @@ class Patcher(private val options: PatcherOptions) { // Determine if merging integrations is required. for (patch in patches) { if (patch.anyRecursively { it.requiresIntegrations }) { - mergeIntegrations = true + context.bytecodeContext.integrations.merge = true 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) { - val apkInfo = ApkInfo(ExtFile(options.inputFile)).also { context.packageMetadata.apkInfo = it } - - // 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() - } + override fun acceptIntegrations(integrations: List) { + context.bytecodeContext.integrations.addAll(integrations) } /** - * 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]. */ - fun executePatches(stopOnError: Boolean = false): Sequence>> { + override fun apply(returnOnError: Boolean) = flow { + class ExecutedPatch(val patchInstance: Patch>, val patchResult: PatchResult) + /** * Execute a [Patch] and its dependencies recursively. * * @param patchClass The [Patch] to execute. - * @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion. + * @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies. * @return The result of executing the [Patch]. */ fun executePatch( - patchClass: Class>, + patchClass: PatchClass, executedPatches: LinkedHashMap ): PatchResult { val patchName = patchClass.patchName - // if the patch has already applied silently skip it - if (executedPatches.contains(patchName)) { - if (!executedPatches[patchName]!!.success) - return PatchResultError("'$patchName' did not succeed previously") + executedPatches[patchName]?.let { executedPatch -> + executedPatch.patchResult.exception ?: return executedPatch.patchResult - logger.trace("Skipping '$patchName' because it has already been applied") - - return PatchResultSuccess() + // 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 PatchResult(patchName, PatchException("'$patchName' did not succeed previously")) } - // recursively execute all dependency patches + // Recursively execute all dependency patches. patchClass.dependencies?.forEach { dependencyClass -> val dependency = dependencyClass.java val result = executePatch(dependency, executedPatches) - if (result.isSuccess()) return@forEach - return PatchResultError( - "'$patchName' depends on '${dependency.patchName}' but the following error was raised: " + - result.error()!!.let { it.cause?.stackTraceToString() ?: it.message } - ) + result.exception?.let { + return PatchResult( + 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() - // TODO: implement this in a more polymorphic way - val patchContext = if (isResourcePatch) { - context.resourceContext - } else { - context.bytecodeContext.also { context -> - (patchInstance as BytecodePatch).fingerprints?.resolveUsingLookupMap(context) - } - } + val patchContext = if (patchInstance is BytecodePatch) { + patchInstance.fingerprints?.resolveUsingLookupMap(context.bytecodeContext) - logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}") + context.bytecodeContext + } else { + context.resourceContext + } return try { - patchInstance.execute(patchContext).also { - executedPatches[patchName] = ExecutedPatch(patchInstance, it.isSuccess()) - } - } catch (e: Exception) { - PatchResultError(e).also { - executedPatches[patchName] = ExecutedPatch(patchInstance, false) - } + patchInstance.execute(patchContext) + + PatchResult(patchName) + } catch (exception: PatchException) { + PatchResult(patchName, exception) + } 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() // 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 { - if (mergeIntegrations) context.integrations.merge(logger) + executedPatches.values + .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) - - // prevent from decoding the manifest twice if it is not needed - if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL) - - logger.info("Executing patches") - - val executedPatches = LinkedHashMap() // 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()!!) + executedPatch.patchResult + } catch (exception: PatchException) { + PatchResult(patchName, exception) + } catch (exception: Exception) { + PatchResult(patchName, PatchException(exception)) } - // TODO: This prints before the patch really finishes in case it is a Closeable - // because the Closeable is closed after all patches are executed. - yield(patch.patchName to result) + result.exception?.let { + emit( + 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 - .filter(ExecutedPatch::success) - .map(ExecutedPatch::patchInstance) - .filterIsInstance(Closeable::class.java) - .asReversed().forEach { - try { - it.close() - } catch (exception: Exception) { - val patchName = (it as Patch).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() - } + override fun close() { + 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 { - /** - * Decode all resources. - */ - FULL, - - /** - * Decode the manifest file only. - */ - MANIFEST_ONLY, - } + override fun get() = PatcherResult( + context.bytecodeContext.get(), + context.resourceContext.get(), + context.packageMetadata.apkInfo.doNotCompress?.toList() + ) } -/** - * 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, val success: Boolean) diff --git a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt index de00605..4aa8396 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt @@ -1,64 +1,37 @@ package app.revanced.patcher -import app.revanced.patcher.data.* -import app.revanced.patcher.logging.Logger +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.Patch -import app.revanced.patcher.util.ClassMerger.merge -import com.android.tools.smali.dexlib2.iface.ClassDef -import java.io.File +import app.revanced.patcher.patch.PatchClass +import brut.androlib.apk.ApkInfo +import brut.directory.ExtFile -data class PatcherContext( - val classes: MutableList, - val resourceCacheDirectory: File, -) { - val packageMetadata = PackageMetadata() - internal val patches = mutableListOf>>() - internal val integrations = Integrations(this) - internal val bytecodeContext = BytecodeContext(classes) - internal val resourceContext = ResourceContext(resourceCacheDirectory) +/** + * A context for ReVanced [Patcher]. + * + * @param options The [PatcherOptions] used to create this context. + */ +class PatcherContext internal constructor(options: PatcherOptions) { + /** + * [PackageMetadata] of the supplied [PatcherOptions.inputFile]. + */ + val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile))) - internal class Integrations(val context: PatcherContext) { - var callback: ((File) -> Unit)? = null - private val integrations: MutableList = mutableListOf() + /** + * The list of [Patch]es to execute. + */ + internal val patches = mutableListOf() - fun add(integrations: List) = 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. - * @param logger A logger. - */ - fun merge(logger: Logger) { - 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 - } - } - } - } - } - } + /** + * The [BytecodeContext] of this [PatcherContext]. + * This holds the current state of the bytecode. + */ + internal val bytecodeContext = BytecodeContext(options) } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt b/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt index b5802ce..aa4d61d 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt @@ -1,25 +1,42 @@ package app.revanced.patcher +import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.logging.Logger import app.revanced.patcher.logging.impl.NopLogger +import brut.androlib.Config import java.io.File /** - * Options for the [Patcher]. - * @param inputFile The input file (usually an apk file). - * @param resourceCacheDirectory Directory to cache resources. - * @param aaptPath Optional path to a custom aapt binary. - * @param frameworkDirectory Optional path to a custom framework directory. - * @param logger Custom logger implementation for the [Patcher]. + * Options for ReVanced [Patcher]. + * @param inputFile The input file to patch. + * @param resourceCachePath The path to the directory to use for caching resources. + * @param aaptBinaryPath The path to a custom aapt binary. + * @param frameworkFileDirectory The path to the directory to cache the framework file in. + * @param logger A [Logger]. */ data class PatcherOptions( internal val inputFile: File, - internal val resourceCacheDirectory: String, - internal val aaptPath: String? = null, - internal val frameworkDirectory: String? = null, + internal val resourceCachePath: File = File("revanced-resource-cache"), + internal val aaptBinaryPath: String? = null, + internal val frameworkFileDirectory: String? = null, 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()) { logger.info("Deleting existing resource cache directory") diff --git a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt b/src/main/kotlin/app/revanced/patcher/PatcherResult.kt index b7007d6..da051a8 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherResult.kt @@ -1,16 +1,23 @@ package app.revanced.patcher -import app.revanced.patcher.util.dex.DexFile import java.io.File +import java.io.InputStream /** * The result of a patcher. * @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 doNotCompress List of relative paths of files to exclude from compressing. */ data class PatcherResult( - val dexFiles: List, - val doNotCompress: List? = null, - val resourceFile: File? -) \ No newline at end of file + val dexFiles: List, + val resourceFile: File?, + val doNotCompress: List? = 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) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt b/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt new file mode 100644 index 0000000..ed7780c --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt @@ -0,0 +1,8 @@ +package app.revanced.patcher + +import app.revanced.patcher.patch.PatchClass + +@FunctionalInterface +interface PatchesConsumer { + fun acceptPatches(patches: List) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt b/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt index 5c59f2d..9f9cd3a 100644 --- a/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt +++ b/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt @@ -18,15 +18,4 @@ annotation class Name( @Target(AnnotationTarget.CLASS) annotation class Description( 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, -) +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt b/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt new file mode 100644 index 0000000..da2a539 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt @@ -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> { + /** + * [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 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 { + options.logger.info("Compiling modified dex files") + + return mutableMapOf().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)) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/Context.kt b/src/main/kotlin/app/revanced/patcher/data/Context.kt index 987c245..d56fb07 100644 --- a/src/main/kotlin/app/revanced/patcher/data/Context.kt +++ b/src/main/kotlin/app/revanced/patcher/data/Context.kt @@ -1,170 +1,9 @@ package app.revanced.patcher.data -import app.revanced.patcher.util.ProxyBackedClassList -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 +import java.util.function.Supplier /** - * A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext]. + * A common interface for contexts such as [ResourceContext] and [BytecodeContext]. */ -sealed interface Context - -class BytecodeContext internal constructor(classes: MutableList) : 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 Iterable.findIndexed(predicate: (T) -> Boolean): Pair? { - 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 { - 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? = 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() - } -} \ No newline at end of file +sealed interface Context : Supplier \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt b/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt new file mode 100644 index 0000000..ad75eb1 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt @@ -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, Iterable { + 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]) + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt index aa194ac..d915f53 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt +++ b/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt @@ -22,8 +22,7 @@ object InstructionExtensions { fun MutableMethodImplementation.addInstructions( index: Int, instructions: List - ) = - instructions.asReversed().forEach { addInstruction(index, it) } + ) = instructions.asReversed().forEach { addInstruction(index, it) } /** * Add instructions to a method. diff --git a/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt index fb25246..493056e 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt +++ b/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt @@ -3,11 +3,10 @@ package app.revanced.patcher.extensions import app.revanced.patcher.annotation.Compatibility import app.revanced.patcher.annotation.Description 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.patch.OptionsContainer import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchClass import app.revanced.patcher.patch.PatchOptions import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.RequiresIntegrations @@ -19,50 +18,43 @@ object PatchExtensions { /** * The name of a [Patch]. */ - val Class>.patchName: String + val PatchClass.patchName: String 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>.version - get() = findAnnotationRecursively(Version::class)?.version - /** * Weather or not a [Patch] should be included. */ - val Class>.include + val PatchClass.include get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include /** * The description of a [Patch]. */ - val Class>.description + val PatchClass.description get() = findAnnotationRecursively(Description::class)?.description /** * The dependencies of a [Patch]. */ - val Class>.dependencies + val PatchClass.dependencies get() = findAnnotationRecursively(DependsOn::class)?.dependencies /** * The packages a [Patch] is compatible with. */ - val Class>.compatiblePackages + val PatchClass.compatiblePackages get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages /** * Weather or not a [Patch] requires integrations. */ - internal val Class>.requiresIntegrations + internal val PatchClass.requiresIntegrations get() = findAnnotationRecursively(RequiresIntegrations::class) != null /** * The options of a [Patch]. */ - val Class>.options: PatchOptions? + val PatchClass.options: PatchOptions? get() = kotlin.companionObject?.let { cl -> if (cl.visibility != KVisibility.PUBLIC) return null kotlin.companionObjectInstance?.let { diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt index a611d9e..4858721 100644 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt @@ -4,7 +4,7 @@ import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod import app.revanced.patcher.fingerprint.Fingerprint 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 com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -99,9 +99,9 @@ abstract class MethodFingerprint( 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 -> 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. */ internal fun Iterable.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) { fingerprint.resolveUsingLookupMap(context) diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 3f982b8..621145b 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -6,20 +6,22 @@ import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import java.io.Closeable +typealias PatchClass = Class>> + /** * A ReVanced patch. * * If it implements [Closeable], it will be closed after all patches have been executed. * Closing will be done in reverse execution order. */ -sealed interface Patch { +sealed interface Patch> { /** * The main function of the [Patch] which the patcher will call. * * @param context The [Context] the patch will work on. * @return The result of executing the patch. */ - fun execute(context: @UnsafeVariance T): PatchResult + fun execute(context: @UnsafeVariance T) } /** diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt new file mode 100644 index 0000000..962e276 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt index b177170..01fab9c 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt @@ -1,35 +1,10 @@ package app.revanced.patcher.patch -interface PatchResult { - fun error(): PatchResultError? { - if (this is PatchResultError) { - return this - } - return 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 \ No newline at end of file +/** + * A result of executing a [Patch]. + * + * @param patchName The name of the [Patch]. + * @param exception The [PatchException] thrown, if any. + */ +@Suppress("MemberVisibilityCanBePrivate") +class PatchResult internal constructor(val patchName: String, val exception: PatchException? = null) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt b/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt index 0ab81be..4e45c38 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt @@ -16,12 +16,12 @@ annotation class Patch(val include: Boolean = true) */ @Target(AnnotationTarget.CLASS) annotation class DependsOn( - val dependencies: Array>> = [] + val dependencies: Array>>> = [] ) - +// TODO: Remove this annotation, once integrations are coupled with patches. /** * Annotation to mark [Patch]es which depend on integrations. */ @Target(AnnotationTarget.CLASS) -annotation class RequiresIntegrations // required because integrations are decoupled from patches \ No newline at end of file +annotation class RequiresIntegrations \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt index f7100f0..b2326eb 100644 --- a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt +++ b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt @@ -1,6 +1,6 @@ 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.logging.Logger 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.isPublic 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.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableField @@ -31,8 +31,9 @@ internal object ClassMerger { * @param otherClass The class to merge with * @param context The context to traverse the class hierarchy in. * @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) //.fixMethodAccess(otherClass, logger) .addMissingFields(otherClass, logger) @@ -89,10 +90,10 @@ internal object ClassMerger { * @param context The context to traverse the class hierarchy in. * @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()) this.asMutableClass().apply { - context.bytecodeContext.traverseClassHierarchy(this) { + context.traverseClassHierarchy(this) { if (accessFlags.isPublic()) return@traverseClassHierarchy logger?.trace("Publicizing ${this.type}") @@ -161,6 +162,19 @@ internal object ClassMerger { } 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() /** diff --git a/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt b/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt new file mode 100644 index 0000000..b679dd9 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt @@ -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? = 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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt b/src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt deleted file mode 100644 index ab305d3..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.revanced.patcher.util - -internal class ListBackedSet(private val list: MutableList) : MutableSet { - override val size get() = list.size - override fun add(element: E) = list.add(element) - override fun addAll(elements: Collection) = 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) = list.removeAll(elements) - override fun retainAll(elements: Collection) = list.retainAll(elements) - override fun contains(element: E) = list.contains(element) - override fun containsAll(elements: Collection) = list.containsAll(elements) - override fun isEmpty() = list.isEmpty() -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt b/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt deleted file mode 100644 index f91cd8d..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt +++ /dev/null @@ -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) : Set { - internal val proxies = mutableListOf() - - /** - * 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) = classes.containsAll(elements) - override fun isEmpty() = classes.isEmpty() - override fun iterator() = classes.iterator() -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt new file mode 100644 index 0000000..681bc8f --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt @@ -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) : MutableSet by classes { + internal val proxies = mutableListOf() + + /** + * 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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt b/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt deleted file mode 100644 index 87b5edc..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/TypeUtil.kt +++ /dev/null @@ -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) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt b/src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt deleted file mode 100644 index ec8ef00..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/dex/DexFile.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt b/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt deleted file mode 100644 index 118bb98..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/patch/PatchBundle.kt +++ /dev/null @@ -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) = 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>) - } - }.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() - ) - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt b/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt index f5697eb..f41a682 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt +++ b/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt @@ -2,19 +2,19 @@ package app.revanced.patcher.usage.bytecode import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Name -import app.revanced.patcher.annotation.Version import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction 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.Patch import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import com.google.common.collect.ImmutableList import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Format 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.value.ImmutableFieldEncodedValue import com.android.tools.smali.dexlib2.util.Preconditions +import com.google.common.collect.ImmutableList @Patch @Name("example-bytecode-patch") @Description("Example demonstration of a bytecode patch.") @ExampleResourceCompatibility -@Version("0.0.1") @DependsOn([ExampleResourcePatch::class]) class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { // This function will be executed by the patcher. // You can treat it as a constructor - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { // Get the resolved method by its fingerprint from the resolver cache 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 """ ) - - // 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() } /** diff --git a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt b/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt index 95a4431..6c45b0a 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt +++ b/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt @@ -2,10 +2,7 @@ package app.revanced.patcher.usage.resource.patch import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Name -import app.revanced.patcher.annotation.Version import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultSuccess import app.revanced.patcher.patch.ResourcePatch import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility @@ -15,9 +12,8 @@ import org.w3c.dom.Element @Name("example-resource-patch") @Description("Example demonstration of a resource patch.") @ExampleResourceCompatibility -@Version("0.0.1") class ExampleResourcePatch : ResourcePatch { - override fun execute(context: ResourceContext): PatchResult { + override fun execute(context: ResourceContext) { context.xmlEditor["AndroidManifest.xml"].use { editor -> val element = editor // regular DomFileEditor .file @@ -29,7 +25,5 @@ class ExampleResourcePatch : ResourcePatch { "exampleValue" ) } - - return PatchResultSuccess() } } \ No newline at end of file