diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 788ba42..8e5d259 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,8 @@ jobs: - name: Make gradlew executable run: chmod +x gradlew - name: Build with Gradle + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew build - name: Setup semantic-release run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog gradle-semantic-release-plugin -D diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..73ab2c8 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f295c2d..192e4c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "1.6.10" + kotlin("jvm") version "1.6.20" java `maven-publish` } @@ -8,23 +8,35 @@ group = "app.revanced" repositories { mavenCentral() + maven { + url = uri("https://maven.pkg.github.com/revanced/multidexlib2") + credentials { + // DO NOT set these variables in the project's gradle.properties. + // Instead, you should set them in: + // Windows: %homepath%\.gradle\gradle.properties + // Linux: ~/.gradle/gradle.properties + username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") // DO NOT CHANGE! + password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE! + } + } } dependencies { implementation(kotlin("stdlib")) - implementation("org.ow2.asm:asm:9.2") - implementation("org.ow2.asm:asm-util:9.2") - implementation("org.ow2.asm:asm-tree:9.2") - implementation("org.ow2.asm:asm-commons:9.2") - implementation("io.github.microutils:kotlin-logging:2.1.21") - testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger! + + api("org.apktool:apktool-lib:2.6.1") + api("app.revanced:multidexlib2:2.5.2.r2") + api("org.smali:smali:2.5.2") + testImplementation(kotlin("test")) } -tasks.test { - useJUnitPlatform() - testLogging { - events("PASSED", "SKIPPED", "FAILED") +tasks { + test { + useJUnitPlatform() + testLogging { + events("PASSED", "SKIPPED", "FAILED") + } } } @@ -33,15 +45,21 @@ java { withJavadocJar() } +val isGitHubCI = System.getenv("GITHUB_ACTOR") != null + publishing { repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/ReVancedTeam/revanced-patcher") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") + if (isGitHubCI) { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } } + } else { + mavenLocal() } } publications { @@ -49,4 +67,4 @@ publishing { from(components["java"]) } } -} +} \ 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 618dbdd..d984438 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,70 +1,209 @@ package app.revanced.patcher -import app.revanced.patcher.cache.Cache -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.resolver.MethodResolver -import app.revanced.patcher.signature.Signature -import app.revanced.patcher.util.Io -import org.objectweb.asm.tree.ClassNode -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream +import app.revanced.patcher.data.PatcherData +import app.revanced.patcher.data.base.Data +import app.revanced.patcher.data.implementation.findIndexed +import app.revanced.patcher.patch.base.Patch +import app.revanced.patcher.patch.implementation.BytecodePatch +import app.revanced.patcher.patch.implementation.ResourcePatch +import app.revanced.patcher.patch.implementation.metadata.PatchMetadata +import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess +import app.revanced.patcher.signature.MethodSignature +import app.revanced.patcher.signature.resolver.SignatureResolver +import app.revanced.patcher.util.ListBackedSet +import brut.androlib.Androlib +import brut.androlib.meta.UsesFramework +import brut.directory.ExtFile +import lanchon.multidexlib2.BasicDexFileNamer +import lanchon.multidexlib2.DexIO +import lanchon.multidexlib2.MultiDexIO +import org.jf.dexlib2.Opcodes +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.iface.DexFile +import org.jf.dexlib2.writer.io.MemoryDataStore +import java.io.File + +val NAMER = BasicDexFileNamer() /** - * The Patcher class. - * ***It is of utmost importance that the input and output streams are NEVER closed.*** - * - * @param input the input stream to read from, must be a JAR - * @param output the output stream to write to - * @param signatures the signatures - * @sample app.revanced.patcher.PatcherTest - * @throws IOException if one of the streams are closed + * The ReVanced Patcher. + * @param inputFile The input file (usually an apk file). + * @param resourceCacheDirectory Directory to cache resources. + * @param patchResources Weather to use the resource patcher. Resources will still need to be decoded. */ class Patcher( - private val input: InputStream, - private val output: OutputStream, - signatures: Array, + inputFile: File, + // TODO: maybe a file system in memory is better. Could cause high memory usage. + private val resourceCacheDirectory: String, + private val patchResources: Boolean = false ) { - var cache: Cache + val packageVersion: String + val packageName: String + + private val usesFramework: UsesFramework + private val patcherData: PatcherData + private val opcodes: Opcodes + private var signaturesResolved = false + private val androlib = Androlib() - private var io: Io - private val patches = mutableListOf() init { - val classes = mutableListOf() - io = Io(input, output, classes) - io.readFromJar() - cache = Cache(classes, MethodResolver(classes, signatures).resolve()) + val extFileInput = ExtFile(inputFile) + val resourceTable = androlib.getResTable(extFileInput, true) + val outDir = File(resourceCacheDirectory) + + if (outDir.exists()) outDir.deleteRecursively() + outDir.mkdir() + + // 1. decode resources to cache directory + androlib.decodeManifestWithResources(extFileInput, outDir, resourceTable) + androlib.decodeResourcesFull(extFileInput, outDir, resourceTable) + + // 2. read framework ids from the resource table + usesFramework = UsesFramework() + usesFramework.ids = resourceTable.listFramePackages().map { it.id }.sorted() + + // 3. read package info + packageName = resourceTable.packageOriginal + packageVersion = resourceTable.versionInfo.versionName + + // read dex files + val dexFile = MultiDexIO.readDexFile(true, inputFile, NAMER, null, null) + opcodes = dexFile.opcodes + + // save to patcher data + patcherData = PatcherData(dexFile.classes.toMutableList(), resourceCacheDirectory) } /** - * Saves the output to the output stream. - * Calling this method will close the input and output streams, - * meaning this method should NEVER be called after. - * - * @throws IOException if one of the streams are closed + * Add additional dex file container to the patcher. + * @param files The dex file containers to add to the patcher. + * @param allowedOverwrites A list of class types that are allowed to be overwritten. + * @param throwOnDuplicates If this is set to true, the patcher will throw an exception if a duplicate class has been found. */ - fun save() { - io.saveAsJar() - } - - fun addPatches(vararg patches: Patch) { - this.patches.addAll(patches) - } - - fun applyPatches(stopOnError: Boolean = false): Map> { - return buildMap { - for (patch in patches) { - val result: Result = try { - val pr = patch.execute(cache) - if (pr.isSuccess()) continue - Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error")) - } catch (e: Exception) { - Result.failure(e) + fun addFiles( + files: Iterable, + allowedOverwrites: Iterable = emptyList(), + throwOnDuplicates: Boolean = false + ) { + for (file in files) { + val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null) + for (classDef in dexFile.classes) { + val e = patcherData.bytecodeData.classes.internalClasses.findIndexed { it.type == classDef.type } + if (e != null) { + if (throwOnDuplicates) { + throw Exception("Class ${classDef.type} has already been added to the patcher.") + } + val (_, idx) = e + if (allowedOverwrites.contains(classDef.type)) { + patcherData.bytecodeData.classes.internalClasses[idx] = classDef + } + continue } - this[patch.patchName] = result - if (stopOnError && result.isFailure) break + patcherData.bytecodeData.classes.internalClasses.add(classDef) } } } -} \ No newline at end of file + + /** + * Save the patched dex file. + */ + fun save(): Map { + val newDexFile = object : DexFile { + override fun getClasses(): Set { + patcherData.bytecodeData.classes.applyProxies() + return ListBackedSet(patcherData.bytecodeData.classes.internalClasses) + } + + override fun getOpcodes(): Opcodes { + return this@Patcher.opcodes + } + } + + // build modified resources + if (patchResources) { + val extDir = ExtFile(resourceCacheDirectory) + androlib.buildResources(extDir, usesFramework) + } + + // write dex modified files + val output = mutableMapOf() + MultiDexIO.writeDexFile( + true, -1, // core count + output, NAMER, newDexFile, + DexIO.DEFAULT_MAX_DEX_POOL_SIZE, + null + ) + return output + } + + /** + * Add a patch to the patcher. + * @param patches The patches to add. + */ + fun addPatches(patches: Iterable>) { + patcherData.patches.addAll(patches) + } + + /** + * Resolves all signatures. + */ + fun resolveSignatures(): List { + val signatures = buildList { + for (patch in patcherData.patches) { + if (patch !is BytecodePatch) continue + this.addAll(patch.signatures) + } + } + if (signatures.isEmpty()) { + return emptyList() + } + + SignatureResolver(patcherData.bytecodeData.classes.internalClasses, signatures).resolve(patcherData) + signaturesResolved = true + return signatures + } + + /** + * Apply patches loaded into the patcher. + * @param stopOnError If true, the patches will stop on the first error. + * @return A map of [PatchResultSuccess]. If the [Patch] was successfully applied, + * [PatchResultSuccess] will always be returned to the wrapping Result object. + * If the [Patch] failed to apply, an Exception will always be returned to the wrapping Result object. + */ + fun applyPatches( + stopOnError: Boolean = false, + callback: (String) -> Unit = {} + ): Map> { + if (!signaturesResolved) { + resolveSignatures() + } + return buildMap { + for (patch in patcherData.patches) { + val resourcePatch = patch is ResourcePatch + if (!patchResources && resourcePatch) continue + + callback(patch.metadata.shortName) + val result: Result = try { + val data = if (resourcePatch) { + patcherData.resourceData + } else { + patcherData.bytecodeData + } + + val pr = patch.execute(data) + + if (pr.isSuccess()) { + Result.success(pr.success()!!) + } else { + Result.failure(Exception(pr.error()?.errorMessage() ?: "Unknown error")) + } + } catch (e: Exception) { + Result.failure(e) + } + this[patch.metadata] = result + if (result.isFailure && stopOnError) break + } + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/cache/Cache.kt b/src/main/kotlin/app/revanced/patcher/cache/Cache.kt deleted file mode 100644 index 3a1d1a9..0000000 --- a/src/main/kotlin/app/revanced/patcher/cache/Cache.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.revanced.patcher.cache - -import org.objectweb.asm.tree.ClassNode - -class Cache( - val classes: List, - val methods: MethodMap -) - -class MethodMap : LinkedHashMap() { - override fun get(key: String): PatchData { - return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache") - } -} - -class MethodNotFoundException(s: String) : Exception(s) diff --git a/src/main/kotlin/app/revanced/patcher/cache/PatchData.kt b/src/main/kotlin/app/revanced/patcher/cache/PatchData.kt deleted file mode 100644 index 033e497..0000000 --- a/src/main/kotlin/app/revanced/patcher/cache/PatchData.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.revanced.patcher.cache - -import app.revanced.patcher.resolver.MethodResolver -import app.revanced.patcher.signature.Signature -import org.objectweb.asm.tree.ClassNode -import org.objectweb.asm.tree.MethodNode - -data class PatchData( - val declaringClass: ClassNode, - val method: MethodNode, - val scanData: PatternScanData -) { - @Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method. - fun findParentMethod(signature: Signature): PatchData? { - return MethodResolver.resolveMethod(declaringClass, signature) - } -} - -data class PatternScanData( - val startIndex: Int, - val endIndex: Int -) diff --git a/src/main/kotlin/app/revanced/patcher/data/PatcherData.kt b/src/main/kotlin/app/revanced/patcher/data/PatcherData.kt new file mode 100644 index 0000000..f225d8f --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/data/PatcherData.kt @@ -0,0 +1,18 @@ +package app.revanced.patcher.data + +import app.revanced.patcher.data.base.Data +import app.revanced.patcher.data.implementation.BytecodeData +import app.revanced.patcher.data.implementation.ResourceData +import app.revanced.patcher.patch.base.Patch +import org.jf.dexlib2.iface.ClassDef +import java.io.File + +internal data class PatcherData( + val internalClasses: MutableList, + val resourceCacheDirectory: String +) { + internal val patches = mutableListOf>() + + internal val bytecodeData = BytecodeData(patches, internalClasses) + internal val resourceData = ResourceData(File(resourceCacheDirectory)) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/base/Data.kt b/src/main/kotlin/app/revanced/patcher/data/base/Data.kt new file mode 100644 index 0000000..95351bb --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/data/base/Data.kt @@ -0,0 +1,9 @@ +package app.revanced.patcher.data.base + +import app.revanced.patcher.data.implementation.BytecodeData +import app.revanced.patcher.data.implementation.ResourceData + +/** + * Constraint interface for [BytecodeData] and [ResourceData] + */ +interface Data \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/implementation/BytecodeData.kt b/src/main/kotlin/app/revanced/patcher/data/implementation/BytecodeData.kt new file mode 100644 index 0000000..2c3e09a --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/data/implementation/BytecodeData.kt @@ -0,0 +1,87 @@ +package app.revanced.patcher.data.implementation + +import app.revanced.patcher.data.base.Data +import app.revanced.patcher.methodWalker.MethodWalker +import app.revanced.patcher.patch.base.Patch +import app.revanced.patcher.patch.implementation.BytecodePatch +import app.revanced.patcher.proxy.ClassProxy +import app.revanced.patcher.signature.SignatureResolverResult +import app.revanced.patcher.util.ProxyBackedClassList +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.iface.Method + +class BytecodeData( + // FIXME: ugly solution due to design. + // It does not make sense for a BytecodeData instance to have access to the patches + private val patches: List>, + internalClasses: MutableList +) : Data { + val classes = ProxyBackedClassList(internalClasses) + + /** + * Find a class by a given class name + * @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 + * @return A proxy for the first class that matches the predicate + */ + fun findClass(predicate: (ClassDef) -> Boolean): ClassProxy? { + // if we already proxied the class matching the predicate... + for (patch in patches) { + if (patch !is BytecodePatch) continue + for (signature in patch.signatures) { + val result = signature.result + result ?: continue + + if (predicate(result.definingClassProxy.immutableClass)) return result.definingClassProxy // ...then return that proxy + } + } + // else resolve the class to a proxy and return it, if the predicate is matching a class + return classes.find(predicate)?.let { + proxy(it) + } + } +} + + +class MethodMap : LinkedHashMap() { + override fun get(key: String): SignatureResolverResult { + return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache") + } +} + +internal class MethodNotFoundException(s: String) : Exception(s) + +internal inline fun Iterable.find(predicate: (T) -> Boolean): T? { + for (element in this) { + if (predicate(element)) { + return element + } + } + return null +} + +fun BytecodeData.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 +} + +fun BytecodeData.proxy(classDef: ClassDef): ClassProxy { + var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type } + if (proxy == null) { + proxy = ClassProxy(classDef) + this.classes.proxies.add(proxy) + } + return proxy +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/implementation/ResourceData.kt b/src/main/kotlin/app/revanced/patcher/data/implementation/ResourceData.kt new file mode 100644 index 0000000..095692f --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/data/implementation/ResourceData.kt @@ -0,0 +1,49 @@ +package app.revanced.patcher.data.implementation + +import app.revanced.patcher.data.base.Data +import org.w3c.dom.Document +import java.io.Closeable +import java.io.File +import javax.xml.XMLConstants +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +class ResourceData(private val resourceCacheDirectory: File) : Data { + private fun resolve(path: String) = resourceCacheDirectory.resolve(path) + + fun forEach(action: (File) -> Unit) = resourceCacheDirectory.walkTopDown().forEach(action) + fun reader(path: String) = resolve(path).reader() + fun writer(path: String) = resolve(path).writer() + + fun replace(path: String, oldValue: String, newValue: String, oldValueIsRegex: Boolean = false) { + // TODO: buffer this somehow + val content = resolve(path).readText() + + if (oldValueIsRegex) { + content.replace(Regex(oldValue), newValue) + return + } + } + + fun getXmlEditor(path: String) = DomFileEditor(resolve(path)) +} + +class DomFileEditor internal constructor(private val domFile: File) : Closeable { + val file: Document + + init { + val factory = DocumentBuilderFactory.newInstance() + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) + + val builder = factory.newDocumentBuilder() + + // this will expectedly throw + file = builder.parse(domFile) + file.normalize() + } + + override fun close() = TransformerFactory.newInstance().newTransformer() + .transform(DOMSource(file), StreamResult(domFile.outputStream())) +} diff --git a/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt new file mode 100644 index 0000000..5e64bdd --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt @@ -0,0 +1,81 @@ +package app.revanced.patcher.extensions + +import app.revanced.patcher.proxy.mutableTypes.MutableMethod.Companion.toMutable +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.builder.BuilderInstruction +import org.jf.dexlib2.builder.MutableMethodImplementation +import org.jf.dexlib2.iface.Method +import org.jf.dexlib2.iface.reference.MethodReference +import org.jf.dexlib2.immutable.ImmutableMethod +import org.jf.dexlib2.immutable.ImmutableMethodImplementation +import org.jf.dexlib2.util.MethodUtil + +infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value +infix fun Int.or(other: AccessFlags) = this or other.value + +fun MutableMethodImplementation.addInstructions(index: Int, instructions: List) { + for (i in instructions.lastIndex downTo 0) { + this.addInstruction(index, instructions[i]) + } +} + +/** + * Clones the method. + * @param registerCount This parameter allows you to change the register count of the method. + * This may be a positive or negative number. + * @return The **immutable** cloned method. Call [toMutable] or [cloneMutable] to get a **mutable** copy. + */ +internal fun Method.clone( + registerCount: Int = 0, +): ImmutableMethod { + val clonedImplementation = implementation?.let { + ImmutableMethodImplementation( + it.registerCount + registerCount, + it.instructions, + it.tryBlocks, + it.debugItems, + ) + } + return ImmutableMethod( + returnType, + name, + parameters, + returnType, + accessFlags, + annotations, + hiddenApiRestrictions, + clonedImplementation + ) +} + +/** + * Clones the method. + * @param registerCount This parameter allows you to change the register count of the method. + * This may be a positive or negative number. + * @return The **mutable** cloned method. Call [clone] to get an **immutable** copy. + */ +internal fun Method.cloneMutable( + registerCount: Int = 0, +) = clone(registerCount).toMutable() + +internal fun Method.softCompareTo( + otherMethod: MethodReference +): Boolean { + if (MethodUtil.isConstructor(this) && !parametersEqual(this.parameterTypes, otherMethod.parameterTypes)) + return false + return this.name == otherMethod.name +} + +// FIXME: also check the order of parameters as different order equals different method overload +internal fun parametersEqual( + parameters1: Iterable, + parameters2: Iterable +): Boolean { + return parameters1.count() == parameters2.count() && parameters1.all { parameter -> + parameters2.any { + it.startsWith( + parameter + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/methodWalker/MethodWalker.kt b/src/main/kotlin/app/revanced/patcher/methodWalker/MethodWalker.kt new file mode 100644 index 0000000..04ed0e7 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/methodWalker/MethodWalker.kt @@ -0,0 +1,55 @@ +package app.revanced.patcher.methodWalker + +import app.revanced.patcher.data.implementation.BytecodeData +import app.revanced.patcher.data.implementation.MethodNotFoundException +import app.revanced.patcher.extensions.softCompareTo +import app.revanced.patcher.proxy.mutableTypes.MutableMethod +import org.jf.dexlib2.Format +import org.jf.dexlib2.iface.Method +import org.jf.dexlib2.iface.instruction.formats.Instruction35c +import org.jf.dexlib2.iface.reference.MethodReference +import org.jf.dexlib2.util.Preconditions + +/** + * Find a method from another method via instruction offsets. + * @param bytecodeData The bytecodeData to use when resolving the next method reference. + * @param currentMethod The method to start from. + */ +class MethodWalker internal constructor( + private val bytecodeData: BytecodeData, + private var currentMethod: Method +) { + /** + * Get the method which was walked last. + * It is possible to cast this method to a [MutableMethod], if the method has been walked mutably. + */ + fun getMethod(): Method { + return currentMethod + } + + /** + * Walk to a method defined at the offset in the instruction list of the current method. + * @param offset The offset of the instruction. This instruction must be of format 35c. + * @param walkMutable If this is true, the class of the method will be resolved mutably. + * The current method will be mutable. + */ + fun walk(offset: Int, walkMutable: Boolean = false): MethodWalker { + currentMethod.implementation?.instructions?.let { instructions -> + val instruction = instructions.elementAt(offset) + + Preconditions.checkFormat(instruction.opcode, Format.Format35c) + + val newMethod = (instruction as Instruction35c).reference as MethodReference + val proxy = bytecodeData.findClass(newMethod.definingClass)!! + + val methods = if (walkMutable) proxy.resolve().methods else proxy.immutableClass.methods + currentMethod = methods.first { it -> + return@first it.softCompareTo(newMethod) + } + return this + } + throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}") + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt deleted file mode 100644 index e2e14ac..0000000 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.revanced.patcher.patch - -import app.revanced.patcher.cache.Cache - -abstract class Patch(val patchName: String) { - abstract fun execute(cache: Cache): PatchResult -} diff --git a/src/main/kotlin/app/revanced/patcher/patch/base/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/base/Patch.kt new file mode 100644 index 0000000..f7f99b2 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/base/Patch.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.patch.base + +import app.revanced.patcher.data.base.Data +import app.revanced.patcher.patch.implementation.BytecodePatch +import app.revanced.patcher.patch.implementation.ResourcePatch +import app.revanced.patcher.patch.implementation.metadata.PatchMetadata +import app.revanced.patcher.patch.implementation.misc.PatchResult + + +/** + * A ReVanced patch. + * Can either be a [ResourcePatch] or a [BytecodePatch] + */ +abstract class Patch( + open val metadata: PatchMetadata +) { + /** + * The main function of the [Patch] which the patcher will call. + */ + abstract fun execute(data: @UnsafeVariance T): PatchResult // FIXME: remove the UnsafeVariance annotation + +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/implementation/BytecodePatch.kt b/src/main/kotlin/app/revanced/patcher/patch/implementation/BytecodePatch.kt new file mode 100644 index 0000000..facbed1 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/implementation/BytecodePatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patcher.patch.implementation + +import app.revanced.patcher.data.implementation.BytecodeData +import app.revanced.patcher.patch.base.Patch +import app.revanced.patcher.patch.implementation.metadata.PatchMetadata +import app.revanced.patcher.signature.MethodSignature + +/** + * Bytecode patch for the Patcher. + * @param metadata [PatchMetadata] for the patch. + * @param signatures A list of [MethodSignature] this patch relies on. + */ +abstract class BytecodePatch( + override val metadata: PatchMetadata, + val signatures: Iterable +) : Patch(metadata) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/implementation/ResourcePatch.kt b/src/main/kotlin/app/revanced/patcher/patch/implementation/ResourcePatch.kt new file mode 100644 index 0000000..bac5820 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/implementation/ResourcePatch.kt @@ -0,0 +1,13 @@ +package app.revanced.patcher.patch.implementation + +import app.revanced.patcher.data.implementation.ResourceData +import app.revanced.patcher.patch.base.Patch +import app.revanced.patcher.patch.implementation.metadata.PatchMetadata + +/** + * Resource patch for the Patcher. + * @param metadata [PatchMetadata] for the patch. + */ +abstract class ResourcePatch( + override val metadata: PatchMetadata +) : Patch(metadata) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/implementation/metadata/PatchMetadata.kt b/src/main/kotlin/app/revanced/patcher/patch/implementation/metadata/PatchMetadata.kt new file mode 100644 index 0000000..cfa8f83 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/implementation/metadata/PatchMetadata.kt @@ -0,0 +1,29 @@ +package app.revanced.patcher.patch.implementation.metadata + +import app.revanced.patcher.patch.base.Patch + +/** + * Metadata about a [Patch]. + * @param shortName A suggestive short name for the [Patch]. + * @param name A suggestive name for the [Patch]. + * @param description A description for the [Patch]. + * @param compatiblePackages A list of packages this [Patch] is compatible with. + * @param version The version of the [Patch]. + */ +data class PatchMetadata( + val shortName: String, + val name: String, + val description: String, + val compatiblePackages: Iterable, + val version: String, +) + +/** + * Metadata about a package. + * @param name The package name. + * @param versions Compatible versions of the package. + */ +data class PackageMetadata( + val name: String, + val versions: Iterable +) \ 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/implementation/misc/PatchResult.kt similarity index 92% rename from src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt rename to src/main/kotlin/app/revanced/patcher/patch/implementation/misc/PatchResult.kt index 02e1ac6..7c0b068 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/implementation/misc/PatchResult.kt @@ -1,4 +1,4 @@ -package app.revanced.patcher.patch +package app.revanced.patcher.patch.implementation.misc interface PatchResult { fun error(): PatchResultError? { diff --git a/src/main/kotlin/app/revanced/patcher/proxy/ClassProxy.kt b/src/main/kotlin/app/revanced/patcher/proxy/ClassProxy.kt new file mode 100644 index 0000000..0aedeef --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/ClassProxy.kt @@ -0,0 +1,41 @@ +package app.revanced.patcher.proxy + +import app.revanced.patcher.proxy.mutableTypes.MutableClass +import org.jf.dexlib2.iface.ClassDef + +/** + * A proxy class for a [ClassDef]. + * + * A class proxy simply holds a reference to the original class + * and allocates a mutable clone for the original class if needed. + * @param immutableClass The class to proxy + */ +class ClassProxy( + val immutableClass: ClassDef, +) { + internal var proxyUsed = false + internal lateinit var mutatedClass: MutableClass + + init { + // in the instance, that a [MutableClass] is being proxied, + // do not create an additional clone and reuse the [MutableClass] instance + if (immutableClass is MutableClass) { + mutatedClass = immutableClass + proxyUsed = true + } + } + + /** + * Allocates and returns a mutable clone of the original class. + * A patch should always use the original immutable class reference + * to avoid unnecessary allocations for the mutable class. + * @return A mutable clone of the original class. + */ + fun resolve(): MutableClass { + if (!proxyUsed) { + proxyUsed = true + mutatedClass = MutableClass(immutableClass) + } + return mutatedClass + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableAnnotation.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableAnnotation.kt new file mode 100644 index 0000000..927f359 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableAnnotation.kt @@ -0,0 +1,29 @@ +package app.revanced.patcher.proxy.mutableTypes + +import app.revanced.patcher.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable +import org.jf.dexlib2.base.BaseAnnotation +import org.jf.dexlib2.iface.Annotation + +class MutableAnnotation(annotation: Annotation) : BaseAnnotation() { + private val visibility = annotation.visibility + private val type = annotation.type + private val _elements by lazy { annotation.elements.map { element -> element.toMutable() }.toMutableSet() } + + override fun getType(): String { + return type + } + + override fun getElements(): MutableSet { + return _elements + } + + override fun getVisibility(): Int { + return visibility + } + + companion object { + fun Annotation.toMutable(): MutableAnnotation { + return MutableAnnotation(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableAnnotationElement.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableAnnotationElement.kt new file mode 100644 index 0000000..85354fe --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableAnnotationElement.kt @@ -0,0 +1,34 @@ +package app.revanced.patcher.proxy.mutableTypes + +import app.revanced.patcher.proxy.mutableTypes.encodedValue.MutableEncodedValue +import app.revanced.patcher.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable +import org.jf.dexlib2.base.BaseAnnotationElement +import org.jf.dexlib2.iface.AnnotationElement +import org.jf.dexlib2.iface.value.EncodedValue + +class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() { + private var name = annotationElement.name + private var value = annotationElement.value.toMutable() + + fun setName(name: String) { + this.name = name + } + + fun setValue(value: MutableEncodedValue) { + this.value = value + } + + override fun getName(): String { + return name + } + + override fun getValue(): EncodedValue { + return value + } + + companion object { + fun AnnotationElement.toMutable(): MutableAnnotationElement { + return MutableAnnotationElement(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableClass.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableClass.kt new file mode 100644 index 0000000..eadab5f --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableClass.kt @@ -0,0 +1,103 @@ +package app.revanced.patcher.proxy.mutableTypes + +import app.revanced.patcher.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import app.revanced.patcher.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.proxy.mutableTypes.MutableMethod.Companion.toMutable +import com.google.common.collect.Iterables +import org.jf.dexlib2.base.reference.BaseTypeReference +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.util.FieldUtil +import org.jf.dexlib2.util.MethodUtil + +class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() { + // Class + private var type = classDef.type + private var sourceFile = classDef.sourceFile + private var accessFlags = classDef.accessFlags + private var superclass = classDef.superclass + + private val _interfaces by lazy { classDef.interfaces.toMutableList() } + private val _annotations by lazy { + classDef.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() + } + + // Methods + private val _methods by lazy { classDef.methods.map { method -> method.toMutable() }.toMutableSet() } + private val _directMethods by lazy { Iterables.filter(_methods, MethodUtil.METHOD_IS_DIRECT).toMutableSet() } + private val _virtualMethods by lazy { Iterables.filter(_methods, MethodUtil.METHOD_IS_VIRTUAL).toMutableSet() } + + // Fields + private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() } + private val _staticFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_STATIC).toMutableSet() } + private val _instanceFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_INSTANCE).toMutableSet() } + + fun setType(type: String) { + this.type = type + } + + fun setSourceFile(sourceFile: String?) { + this.sourceFile = sourceFile + } + + fun setAccessFlags(accessFlags: Int) { + this.accessFlags = accessFlags + } + + fun setSuperClass(superclass: String?) { + this.superclass = superclass + } + + override fun getType(): String { + return type + } + + override fun getAccessFlags(): Int { + return accessFlags + } + + override fun getSourceFile(): String? { + return sourceFile + } + + override fun getSuperclass(): String? { + return superclass + } + + override fun getInterfaces(): MutableList { + return _interfaces + } + + override fun getAnnotations(): MutableSet { + return _annotations + } + + override fun getStaticFields(): MutableSet { + return _staticFields + } + + override fun getInstanceFields(): MutableSet { + return _instanceFields + } + + override fun getFields(): MutableSet { + return _fields + } + + override fun getDirectMethods(): MutableSet { + return _directMethods + } + + override fun getVirtualMethods(): MutableSet { + return _virtualMethods + } + + override fun getMethods(): MutableSet { + return _methods + } + + companion object { + fun ClassDef.toMutable(): MutableClass { + return MutableClass(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableField.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableField.kt new file mode 100644 index 0000000..34c445a --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableField.kt @@ -0,0 +1,73 @@ +package app.revanced.patcher.proxy.mutableTypes + +import app.revanced.patcher.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import app.revanced.patcher.proxy.mutableTypes.encodedValue.MutableEncodedValue +import app.revanced.patcher.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable +import org.jf.dexlib2.HiddenApiRestriction +import org.jf.dexlib2.base.reference.BaseFieldReference +import org.jf.dexlib2.iface.Field + +class MutableField(field: Field) : Field, BaseFieldReference() { + private var definingClass = field.definingClass + private var name = field.name + private var type = field.type + private var accessFlags = field.accessFlags + + private var initialValue = field.initialValue?.toMutable() + private val _annotations by lazy { field.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() } + private val _hiddenApiRestrictions by lazy { field.hiddenApiRestrictions } + + fun setDefiningClass(definingClass: String) { + this.definingClass = definingClass + } + + fun setName(name: String) { + this.name = name + } + + fun setType(type: String) { + this.type = type + } + + fun setAccessFlags(accessFlags: Int) { + this.accessFlags = accessFlags + } + + fun setInitialValue(initialValue: MutableEncodedValue?) { + this.initialValue = initialValue + } + + override fun getDefiningClass(): String { + return this.definingClass + } + + override fun getName(): String { + return this.name + } + + override fun getType(): String { + return this.type + } + + override fun getAnnotations(): MutableSet { + return this._annotations + } + + override fun getAccessFlags(): Int { + return this.accessFlags + } + + override fun getHiddenApiRestrictions(): MutableSet { + return this._hiddenApiRestrictions + } + + override fun getInitialValue(): MutableEncodedValue? { + return this.initialValue + } + + companion object { + fun Field.toMutable(): MutableField { + return MutableField(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableMethod.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableMethod.kt new file mode 100644 index 0000000..2a778a5 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableMethod.kt @@ -0,0 +1,64 @@ +package app.revanced.patcher.proxy.mutableTypes + +import app.revanced.patcher.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import app.revanced.patcher.proxy.mutableTypes.MutableMethodParameter.Companion.toMutable +import org.jf.dexlib2.HiddenApiRestriction +import org.jf.dexlib2.base.reference.BaseMethodReference +import org.jf.dexlib2.builder.MutableMethodImplementation +import org.jf.dexlib2.iface.Method + +class MutableMethod(method: Method) : Method, BaseMethodReference() { + private var definingClass = method.definingClass + private var name = method.name + private var accessFlags = method.accessFlags + private var returnType = method.returnType + + // Create own mutable MethodImplementation (due to not being able to change members like register count) + private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } } + private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() } + private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() } + private val _parameterTypes by lazy { method.parameterTypes.toMutableList() } + private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions } + + override fun getDefiningClass(): String { + return definingClass + } + + override fun getName(): String { + return name + } + + override fun getParameterTypes(): MutableList { + return _parameterTypes + } + + override fun getReturnType(): String { + return returnType + } + + override fun getAnnotations(): MutableSet { + return _annotations + } + + override fun getAccessFlags(): Int { + return accessFlags + } + + override fun getHiddenApiRestrictions(): MutableSet { + return _hiddenApiRestrictions + } + + override fun getParameters(): MutableList { + return _parameters + } + + override fun getImplementation(): MutableMethodImplementation? { + return _implementation + } + + companion object { + fun Method.toMutable(): MutableMethod { + return MutableMethod(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableMethodParameter.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableMethodParameter.kt new file mode 100644 index 0000000..3b5287f --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableMethodParameter.kt @@ -0,0 +1,37 @@ +package app.revanced.patcher.proxy.mutableTypes + +import app.revanced.patcher.proxy.mutableTypes.MutableAnnotation.Companion.toMutable +import org.jf.dexlib2.base.BaseMethodParameter +import org.jf.dexlib2.iface.MethodParameter + +// TODO: finish overriding all members if necessary +class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() { + private var type = parameter.type + private var name = parameter.name + private var signature = parameter.signature + private val _annotations by lazy { + parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() + } + + override fun getType(): String { + return type + } + + override fun getName(): String? { + return name + } + + override fun getSignature(): String? { + return signature + } + + override fun getAnnotations(): MutableSet { + return _annotations + } + + companion object { + fun MethodParameter.toMutable(): MutableMethodParameter { + return MutableMethodParameter(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt new file mode 100644 index 0000000..e62e573 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt @@ -0,0 +1,33 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import app.revanced.patcher.proxy.mutableTypes.MutableAnnotationElement.Companion.toMutable +import org.jf.dexlib2.base.value.BaseAnnotationEncodedValue +import org.jf.dexlib2.iface.AnnotationElement +import org.jf.dexlib2.iface.value.AnnotationEncodedValue + +class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) : BaseAnnotationEncodedValue(), + MutableEncodedValue { + private var type = annotationEncodedValue.type + + private val _elements by lazy { + annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet() + } + + override fun getType(): String { + return this.type + } + + fun setType(type: String) { + this.type = type + } + + override fun getElements(): MutableSet { + return _elements + } + + companion object { + fun AnnotationEncodedValue.toMutable(): MutableAnnotationEncodedValue { + return MutableAnnotationEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt new file mode 100644 index 0000000..268e78e --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import app.revanced.patcher.proxy.mutableTypes.encodedValue.MutableEncodedValue.Companion.toMutable +import org.jf.dexlib2.base.value.BaseArrayEncodedValue +import org.jf.dexlib2.iface.value.ArrayEncodedValue +import org.jf.dexlib2.iface.value.EncodedValue + +class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) : BaseArrayEncodedValue(), MutableEncodedValue { + private val _value by lazy { + arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList() + } + + override fun getValue(): MutableList { + return _value + } + + companion object { + fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue { + return MutableArrayEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt new file mode 100644 index 0000000..d6377af --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt @@ -0,0 +1,23 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseBooleanEncodedValue +import org.jf.dexlib2.iface.value.BooleanEncodedValue + +class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) : BaseBooleanEncodedValue(), + MutableEncodedValue { + private var value = booleanEncodedValue.value + + override fun getValue(): Boolean { + return this.value + } + + fun setValue(value: Boolean) { + this.value = value + } + + companion object { + fun BooleanEncodedValue.toMutable(): MutableBooleanEncodedValue { + return MutableBooleanEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt new file mode 100644 index 0000000..61757c4 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseByteEncodedValue +import org.jf.dexlib2.iface.value.ByteEncodedValue + +class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) : BaseByteEncodedValue(), MutableEncodedValue { + private var value = byteEncodedValue.value + + override fun getValue(): Byte { + return this.value + } + + fun setValue(value: Byte) { + this.value = value + } + + companion object { + fun ByteEncodedValue.toMutable(): MutableByteEncodedValue { + return MutableByteEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt new file mode 100644 index 0000000..c3cb3ee --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseCharEncodedValue +import org.jf.dexlib2.iface.value.CharEncodedValue + +class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) : BaseCharEncodedValue(), MutableEncodedValue { + private var value = charEncodedValue.value + + override fun getValue(): Char { + return this.value + } + + fun setValue(value: Char) { + this.value = value + } + + companion object { + fun CharEncodedValue.toMutable(): MutableCharEncodedValue { + return MutableCharEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt new file mode 100644 index 0000000..ddfb07b --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt @@ -0,0 +1,23 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseDoubleEncodedValue +import org.jf.dexlib2.iface.value.DoubleEncodedValue + +class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) : BaseDoubleEncodedValue(), + MutableEncodedValue { + private var value = doubleEncodedValue.value + + override fun getValue(): Double { + return this.value + } + + fun setValue(value: Double) { + this.value = value + } + + companion object { + fun DoubleEncodedValue.toMutable(): MutableDoubleEncodedValue { + return MutableDoubleEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt new file mode 100644 index 0000000..0f170bf --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt @@ -0,0 +1,32 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.ValueType +import org.jf.dexlib2.iface.value.* + +interface MutableEncodedValue : EncodedValue { + companion object { + fun EncodedValue.toMutable(): MutableEncodedValue { + return when (this.valueType) { + ValueType.TYPE -> MutableTypeEncodedValue(this as TypeEncodedValue) + ValueType.FIELD -> MutableFieldEncodedValue(this as FieldEncodedValue) + ValueType.METHOD -> MutableMethodEncodedValue(this as MethodEncodedValue) + ValueType.ENUM -> MutableEnumEncodedValue(this as EnumEncodedValue) + ValueType.ARRAY -> MutableArrayEncodedValue(this as ArrayEncodedValue) + ValueType.ANNOTATION -> MutableAnnotationEncodedValue(this as AnnotationEncodedValue) + ValueType.BYTE -> MutableByteEncodedValue(this as ByteEncodedValue) + ValueType.SHORT -> MutableShortEncodedValue(this as ShortEncodedValue) + ValueType.CHAR -> MutableCharEncodedValue(this as CharEncodedValue) + ValueType.INT -> MutableIntEncodedValue(this as IntEncodedValue) + ValueType.LONG -> MutableLongEncodedValue(this as LongEncodedValue) + ValueType.FLOAT -> MutableFloatEncodedValue(this as FloatEncodedValue) + ValueType.DOUBLE -> MutableDoubleEncodedValue(this as DoubleEncodedValue) + ValueType.METHOD_TYPE -> MutableMethodTypeEncodedValue(this as MethodTypeEncodedValue) + ValueType.METHOD_HANDLE -> MutableMethodHandleEncodedValue(this as MethodHandleEncodedValue) + ValueType.STRING -> MutableStringEncodedValue(this as StringEncodedValue) + ValueType.BOOLEAN -> MutableBooleanEncodedValue(this as BooleanEncodedValue) + ValueType.NULL -> MutableNullEncodedValue() + else -> this as MutableEncodedValue + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt new file mode 100644 index 0000000..d9fbdb9 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt @@ -0,0 +1,23 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseEnumEncodedValue +import org.jf.dexlib2.iface.reference.FieldReference +import org.jf.dexlib2.iface.value.EnumEncodedValue + +class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) : BaseEnumEncodedValue(), MutableEncodedValue { + private var value = enumEncodedValue.value + + override fun getValue(): FieldReference { + return this.value + } + + fun setValue(value: FieldReference) { + this.value = value + } + + companion object { + fun EnumEncodedValue.toMutable(): MutableEnumEncodedValue { + return MutableEnumEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt new file mode 100644 index 0000000..34d9b90 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt @@ -0,0 +1,28 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.ValueType +import org.jf.dexlib2.base.value.BaseFieldEncodedValue +import org.jf.dexlib2.iface.reference.FieldReference +import org.jf.dexlib2.iface.value.FieldEncodedValue + +class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) : BaseFieldEncodedValue(), MutableEncodedValue { + private var value = fieldEncodedValue.value + + override fun getValueType(): Int { + return ValueType.FIELD + } + + override fun getValue(): FieldReference { + return this.value + } + + fun setValue(value: FieldReference) { + this.value = value + } + + companion object { + fun FieldEncodedValue.toMutable(): MutableFieldEncodedValue { + return MutableFieldEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt new file mode 100644 index 0000000..c2fff54 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseFloatEncodedValue +import org.jf.dexlib2.iface.value.FloatEncodedValue + +class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) : BaseFloatEncodedValue(), MutableEncodedValue { + private var value = floatEncodedValue.value + + override fun getValue(): Float { + return this.value + } + + fun setValue(value: Float) { + this.value = value + } + + companion object { + fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue { + return MutableFloatEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt new file mode 100644 index 0000000..d5918c6 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseIntEncodedValue +import org.jf.dexlib2.iface.value.IntEncodedValue + +class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) : BaseIntEncodedValue(), MutableEncodedValue { + private var value = intEncodedValue.value + + override fun getValue(): Int { + return this.value + } + + fun setValue(value: Int) { + this.value = value + } + + companion object { + fun IntEncodedValue.toMutable(): MutableIntEncodedValue { + return MutableIntEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt new file mode 100644 index 0000000..190a2d8 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseLongEncodedValue +import org.jf.dexlib2.iface.value.LongEncodedValue + +class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) : BaseLongEncodedValue(), MutableEncodedValue { + private var value = longEncodedValue.value + + override fun getValue(): Long { + return this.value + } + + fun setValue(value: Long) { + this.value = value + } + + companion object { + fun LongEncodedValue.toMutable(): MutableLongEncodedValue { + return MutableLongEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt new file mode 100644 index 0000000..24f0ba0 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt @@ -0,0 +1,24 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseMethodEncodedValue +import org.jf.dexlib2.iface.reference.MethodReference +import org.jf.dexlib2.iface.value.MethodEncodedValue + +class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) : BaseMethodEncodedValue(), + MutableEncodedValue { + private var value = methodEncodedValue.value + + override fun getValue(): MethodReference { + return this.value + } + + fun setValue(value: MethodReference) { + this.value = value + } + + companion object { + fun MethodEncodedValue.toMutable(): MutableMethodEncodedValue { + return MutableMethodEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt new file mode 100644 index 0000000..54ba088 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt @@ -0,0 +1,27 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseMethodHandleEncodedValue +import org.jf.dexlib2.iface.reference.MethodHandleReference +import org.jf.dexlib2.iface.value.MethodHandleEncodedValue + +class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEncodedValue) : + BaseMethodHandleEncodedValue(), + MutableEncodedValue { + private var value = methodHandleEncodedValue.value + + override fun getValue(): MethodHandleReference { + return this.value + } + + fun setValue(value: MethodHandleReference) { + this.value = value + } + + companion object { + fun MethodHandleEncodedValue.toMutable(): MutableMethodHandleEncodedValue { + return MutableMethodHandleEncodedValue(this) + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt new file mode 100644 index 0000000..dded228 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt @@ -0,0 +1,26 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseMethodTypeEncodedValue +import org.jf.dexlib2.iface.reference.MethodProtoReference +import org.jf.dexlib2.iface.value.MethodTypeEncodedValue + +class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(), + MutableEncodedValue { + private var value = methodTypeEncodedValue.value + + override fun getValue(): MethodProtoReference { + return this.value + } + + fun setValue(value: MethodProtoReference) { + this.value = value + } + + companion object { + fun MethodTypeEncodedValue.toMutable(): MutableMethodTypeEncodedValue { + return MutableMethodTypeEncodedValue(this) + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt new file mode 100644 index 0000000..cce128d --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt @@ -0,0 +1,12 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseNullEncodedValue +import org.jf.dexlib2.iface.value.ByteEncodedValue + +class MutableNullEncodedValue : BaseNullEncodedValue(), MutableEncodedValue { + companion object { + fun ByteEncodedValue.toMutable(): MutableByteEncodedValue { + return MutableByteEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt new file mode 100644 index 0000000..e8c0f74 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseShortEncodedValue +import org.jf.dexlib2.iface.value.ShortEncodedValue + +class MutableShortEncodedValue(shortEncodedValue: ShortEncodedValue) : BaseShortEncodedValue(), MutableEncodedValue { + private var value = shortEncodedValue.value + + override fun getValue(): Short { + return this.value + } + + fun setValue(value: Short) { + this.value = value + } + + companion object { + fun ShortEncodedValue.toMutable(): MutableShortEncodedValue { + return MutableShortEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt new file mode 100644 index 0000000..f02acc4 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt @@ -0,0 +1,24 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseStringEncodedValue +import org.jf.dexlib2.iface.value.ByteEncodedValue +import org.jf.dexlib2.iface.value.StringEncodedValue + +class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) : BaseStringEncodedValue(), + MutableEncodedValue { + private var value = stringEncodedValue.value + + override fun getValue(): String { + return this.value + } + + fun setValue(value: String) { + this.value = value + } + + companion object { + fun ByteEncodedValue.toMutable(): MutableByteEncodedValue { + return MutableByteEncodedValue(this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt new file mode 100644 index 0000000..6f56a33 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt @@ -0,0 +1,22 @@ +package app.revanced.patcher.proxy.mutableTypes.encodedValue + +import org.jf.dexlib2.base.value.BaseTypeEncodedValue +import org.jf.dexlib2.iface.value.TypeEncodedValue + +class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) : BaseTypeEncodedValue(), MutableEncodedValue { + private var value = typeEncodedValue.value + + override fun getValue(): String { + return this.value + } + + fun setValue(value: String) { + this.value = value + } + + companion object { + fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue { + return MutableTypeEncodedValue(this) + } + } +} diff --git a/src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt b/src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt deleted file mode 100644 index ca0a41a..0000000 --- a/src/main/kotlin/app/revanced/patcher/resolver/MethodResolver.kt +++ /dev/null @@ -1,156 +0,0 @@ -package app.revanced.patcher.resolver - -import app.revanced.patcher.cache.MethodMap -import app.revanced.patcher.cache.PatchData -import app.revanced.patcher.cache.PatternScanData -import app.revanced.patcher.signature.Signature -import app.revanced.patcher.util.ExtraTypes -import mu.KotlinLogging -import org.objectweb.asm.Type -import org.objectweb.asm.tree.* - -private val logger = KotlinLogging.logger("MethodResolver") - -internal class MethodResolver(private val classList: List, private val signatures: Array) { - fun resolve(): MethodMap { - val methodMap = MethodMap() - - for ((classNode, methods) in classList) { - for (method in methods) { - for (signature in signatures) { - if (methodMap.containsKey(signature.name)) { // method already found for this sig - logger.trace { "Sig ${signature.name} already found, skipping." } - continue - } - logger.trace { "Resolving sig ${signature.name}: ${classNode.name} / ${method.name}" } - val (r, sr) = cmp(method, signature) - if (!r || sr == null) { - logger.trace { "Compare result for sig ${signature.name} has failed!" } - continue - } - logger.trace { "Method for sig ${signature.name} found!" } - methodMap[signature.name] = PatchData( - classNode, - method, - PatternScanData( - // sadly we cannot create contracts for a data class, so we must assert - sr.startIndex!!, - sr.endIndex!! - ) - ) - } - } - } - - for (signature in signatures) { - if (methodMap.containsKey(signature.name)) continue - logger.error { "Could not find method for sig ${signature.name}!" } - } - - return methodMap - } - - // These functions do not require the constructor values, so they can be static. - companion object { - fun resolveMethod(classNode: ClassNode, signature: Signature): PatchData? { - for (method in classNode.methods) { - val (r, sr) = cmp(method, signature) - if (!r || sr == null) continue - return PatchData( - classNode, - method, - PatternScanData(0, 0) // opcode list is always ignored. - ) - } - return null - } - - private fun cmp(method: MethodNode, signature: Signature): Pair { - signature.returns?.let { _ -> - val methodReturns = Type.getReturnType(method.desc).convertObject() - if (signature.returns != methodReturns) { - logger.trace { - """ - Comparing sig ${signature.name}: invalid return type: - expected ${signature.returns}, - got $methodReturns - """.trimIndent() - } - return@cmp false to null - } - } - - signature.accessors?.let { _ -> - if (signature.accessors != method.access) { - logger.trace { - """ - Comparing sig ${signature.name}: invalid accessors: - expected ${signature.accessors}, - got ${method.access} - """.trimIndent() - } - return@cmp false to null - } - } - - signature.parameters?.let { _ -> - val parameters = Type.getArgumentTypes(method.desc).convertObjects() - if (!signature.parameters.contentEquals(parameters)) { - logger.trace { - """ - Comparing sig ${signature.name}: invalid parameter types: - expected ${signature.parameters.joinToString()}}, - got ${parameters.joinToString()} - """.trimIndent() - } - return@cmp false to null - } - } - - signature.opcodes?.let { _ -> - val result = method.instructions.scanFor(signature.opcodes) - if (!result.found) { - logger.trace { "Comparing sig ${signature.name}: invalid opcode pattern" } - return@cmp false to null - } - return@cmp true to result - } - - return true to ScanResult(true) - } - } -} - -private operator fun ClassNode.component1() = this -private operator fun ClassNode.component2() = this.methods - -private fun InsnList.scanFor(pattern: IntArray): ScanResult { - for (i in 0 until this.size()) { - var occurrence = 0 - while (i + occurrence < this.size()) { - val n = this[i + occurrence] - if (!n.shouldSkip() && n.opcode != pattern[occurrence]) break - if (++occurrence >= pattern.size) { - val current = i + occurrence - return ScanResult(true, current - pattern.size, current) - } - } - } - - return ScanResult(false) -} - -private fun Type.convertObject(): Type { - return when (this.sort) { - Type.OBJECT -> ExtraTypes.Any - Type.ARRAY -> ExtraTypes.ArrayAny - else -> this - } -} - -private fun Array.convertObjects(): Array { - return this.map { it.convertObject() }.toTypedArray() -} - -private fun AbstractInsnNode.shouldSkip() = - type == AbstractInsnNode.LABEL || type == AbstractInsnNode.LINE diff --git a/src/main/kotlin/app/revanced/patcher/resolver/ScanResult.kt b/src/main/kotlin/app/revanced/patcher/resolver/ScanResult.kt deleted file mode 100644 index 135b0ca..0000000 --- a/src/main/kotlin/app/revanced/patcher/resolver/ScanResult.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.revanced.patcher.resolver - -internal data class ScanResult( - val found: Boolean, - val startIndex: Int? = 0, - val endIndex: Int? = 0 -) diff --git a/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt new file mode 100644 index 0000000..1e7b13a --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt @@ -0,0 +1,111 @@ +package app.revanced.patcher.signature + +import app.revanced.patcher.data.implementation.MethodNotFoundException +import app.revanced.patcher.patch.implementation.metadata.PackageMetadata +import org.jf.dexlib2.Opcode + +/** + * Represents the [MethodSignature] for a method. + * @param metadata Metadata for this [MethodSignature]. + * @param returnType The return type of the method. + * @param accessFlags The access flags of the method. + * @param methodParameters The parameters of the method. + * @param opcodes The list of opcodes of the method. + * @param strings A list of strings which a method contains. + * A `null` opcode is equals to an unknown opcode. + */ +class MethodSignature( + val metadata: MethodSignatureMetadata, + internal val returnType: String?, + internal val accessFlags: Int?, + internal val methodParameters: Iterable?, + internal val opcodes: Iterable?, + internal val strings: Iterable? = null +) { + /** + * The result of the signature + */ + var result: SignatureResolverResult? = null + get() { + return field ?: throw MethodNotFoundException( + "Could not resolve required signature ${metadata.name}" + ) + } + val resolved: Boolean + get() { + var resolved = false + try { + resolved = result != null + } catch (_: Exception) { + } + return resolved + } +} + +/** + * Metadata about a [MethodSignature]. + * @param name A suggestive name for the [MethodSignature]. + * @param methodMetadata Metadata about the method for the [MethodSignature]. + * @param patternScanMethod The pattern scanning method the pattern scanner should rely on. + * Can either be [PatternScanMethod.Fuzzy] or [PatternScanMethod.Direct]. + * @param description An optional description for the [MethodSignature]. + * @param compatiblePackages The list of packages the [MethodSignature] is compatible with. + * @param version The version of this signature. + */ +data class MethodSignatureMetadata( + val name: String, + val methodMetadata: MethodMetadata?, + val patternScanMethod: PatternScanMethod, + val compatiblePackages: Iterable, + val description: String?, + val version: String +) + +/** + * Metadata about the method for a [MethodSignature]. + * @param definingClass The defining class name of the method. + * @param name A suggestive name for the method which the [MethodSignature] was created for. + */ +data class MethodMetadata( + val definingClass: String?, + val name: String? +) + +/** + * The method, the patcher should rely on when scanning the opcode pattern of a [MethodSignature] + */ +interface PatternScanMethod { + /** + * When comparing the signature, if one or more of the opcodes do not match, skip. + */ + class Direct : PatternScanMethod + + /** + * When comparing the signature, if [threshold] or more of the opcodes do not match, skip. + */ + class Fuzzy(internal val threshold: Int) : PatternScanMethod { + /** + * A list of warnings the resolver found. + * + * This list will be allocated when the signature has been found. + * Meaning, if the signature was not found, + * or the signature was not yet resolved, + * the list will be null. + */ + var warnings: List? = null + + /** + * Represents a resolver warning. + * @param correctOpcode The opcode the instruction list has. + * @param wrongOpcode The opcode the pattern list of the signature currently has. + * @param instructionIndex The index of the opcode relative to the instruction list. + * @param patternIndex The index of the opcode relative to the pattern list from the signature. + */ + data class Warning( + val correctOpcode: Opcode, + val wrongOpcode: Opcode, + val instructionIndex: Int, + val patternIndex: Int, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/signature/Signature.kt b/src/main/kotlin/app/revanced/patcher/signature/Signature.kt deleted file mode 100644 index 19f303d..0000000 --- a/src/main/kotlin/app/revanced/patcher/signature/Signature.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.revanced.patcher.signature - -import org.objectweb.asm.Type - -/** - * An ASM signature list for the Patcher. - * - * @param name The name of the method. - * Do not use the actual method name, instead try to guess what the method name originally was. - * If you are unable to guess a method name, doing something like "patch-name-1" is fine too. - * For example: "override-codec-1". - * This method name will be mapped to the method matching the signature. - * Even though this is technically not needed for the `findParentMethod` method, - * it is still recommended giving the method a name, so it can be identified easily. - * @param returns The return type/signature of the method. - * @param accessors The accessors of the method. - * @param parameters The parameter types of the method. - * @param opcodes The opcode pattern of the method, used to find the method by pattern scanning. - */ -@Suppress("ArrayInDataClass") -data class Signature( - val name: String, - val returns: Type?, - val accessors: Int?, - val parameters: Array?, - val opcodes: IntArray? -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/signature/SignatureResolverResult.kt b/src/main/kotlin/app/revanced/patcher/signature/SignatureResolverResult.kt new file mode 100644 index 0000000..f3e4c5b --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/signature/SignatureResolverResult.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.signature + +import app.revanced.patcher.extensions.softCompareTo +import app.revanced.patcher.proxy.ClassProxy +import app.revanced.patcher.signature.resolver.SignatureResolver +import org.jf.dexlib2.iface.Method + +/** + * Represents the result of a [SignatureResolver]. + * @param definingClassProxy The [ClassProxy] that the matching method was found in. + * @param resolvedMethod The actual matching method. + * @param scanData Opcodes pattern scan result. + */ +data class SignatureResolverResult( + val definingClassProxy: ClassProxy, + val scanData: PatternScanResult, + private val resolvedMethod: Method, +) { + /** + * Returns the **mutable** method by the [resolvedMethod] from the [definingClassProxy]. + * + * Please note, this method allocates a [ClassProxy]. + * Use [immutableMethod] where possible. + */ + val method + get() = definingClassProxy.resolve().methods.first { + it.softCompareTo(resolvedMethod) + } + + /** + * Returns the **immutable** method by the [resolvedMethod] from the [definingClassProxy]. + * + * If you need to modify the method, use [method] instead. + */ + val immutableMethod: Method + get() = definingClassProxy.immutableClass.methods.first { + it.softCompareTo(resolvedMethod) + } + + fun findParentMethod(signature: MethodSignature): SignatureResolverResult? { + return SignatureResolver.resolveFromProxy(definingClassProxy, signature) + } +} + +data class PatternScanResult( + val startIndex: Int, + val endIndex: Int +) diff --git a/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt b/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt new file mode 100644 index 0000000..c7d2d0d --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt @@ -0,0 +1,167 @@ +package app.revanced.patcher.signature.resolver + +import app.revanced.patcher.data.PatcherData +import app.revanced.patcher.data.implementation.proxy +import app.revanced.patcher.extensions.parametersEqual +import app.revanced.patcher.proxy.ClassProxy +import app.revanced.patcher.signature.MethodSignature +import app.revanced.patcher.signature.PatternScanMethod +import app.revanced.patcher.signature.PatternScanResult +import app.revanced.patcher.signature.SignatureResolverResult +import org.jf.dexlib2.Opcode +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.iface.Method +import org.jf.dexlib2.iface.instruction.Instruction +import org.jf.dexlib2.iface.instruction.formats.Instruction21c +import org.jf.dexlib2.iface.reference.StringReference + +internal class SignatureResolver( + private val classes: List, + private val methodSignatures: Iterable +) { + fun resolve(patcherData: PatcherData) { + for (signature in methodSignatures) { + for (classDef in classes) { + for (method in classDef.methods) { + val patternScanData = compareSignatureToMethod(signature, method) ?: continue + + // create class proxy, in case a patch needs mutability + val classProxy = patcherData.bytecodeData.proxy(classDef) + signature.result = SignatureResolverResult( + classProxy, + patternScanData, + method, + ) + } + } + } + } + + // These functions do not require the constructor values, so they can be static. + companion object { + fun resolveFromProxy(classProxy: ClassProxy, signature: MethodSignature): SignatureResolverResult? { + for (method in classProxy.immutableClass.methods) { + val result = compareSignatureToMethod(signature, method) ?: continue + return SignatureResolverResult( + classProxy, + result, + method, + ) + } + return null + } + + private fun compareSignatureToMethod( + signature: MethodSignature, + method: Method + ): PatternScanResult? { + signature.returnType?.let { + if (!method.returnType.startsWith(signature.returnType)) { + return null + } + } + + signature.accessFlags?.let { + if (signature.accessFlags != method.accessFlags) { + return null + } + } + + signature.methodParameters?.let { + if (!parametersEqual(signature.methodParameters, method.parameterTypes)) { + return null + } + } + + signature.strings?.let { strings -> + method.implementation ?: return null + + method.implementation!!.instructions.let { instructions -> + val stringsList = strings.toMutableList() + + for (instruction in instructions) { + if (instruction.opcode != Opcode.CONST_STRING) continue + + val string = ((instruction as Instruction21c).reference as StringReference).string + val i = stringsList.indexOfFirst { it == string } + if (i != -1) stringsList.removeAt(i) + } + + if (stringsList.isNotEmpty()) return null + } + } + + return if (signature.opcodes == null) { + PatternScanResult(0, 0) + } else { + method.implementation?.instructions?.let { + compareOpcodes(signature, it) + } + } + } + + private fun compareOpcodes( + signature: MethodSignature, + instructions: Iterable + ): PatternScanResult? { + val count = instructions.count() + val pattern = signature.opcodes!! + val size = pattern.count() + val method = signature.metadata.patternScanMethod + val threshold = if (method is PatternScanMethod.Fuzzy) + method.threshold else 0 + + for (instructionIndex in 0 until count) { + var patternIndex = 0 + var currentThreshold = threshold + while (instructionIndex + patternIndex < count) { + val originalOpcode = instructions.elementAt(instructionIndex + patternIndex).opcode + val patternOpcode = pattern.elementAt(patternIndex) + if ( + patternOpcode != null && // unknown opcode + originalOpcode != patternOpcode && + currentThreshold-- == 0 + ) break + if (++patternIndex < size) continue + patternIndex-- // fix pattern offset + + val result = PatternScanResult(instructionIndex, instructionIndex + patternIndex) + if (method is PatternScanMethod.Fuzzy) { + method.warnings = generateWarnings( + signature, instructions, result + ) + } + return result + } + } + + return null + } + + private fun generateWarnings( + signature: MethodSignature, + instructions: Iterable, + scanResult: PatternScanResult, + ) = buildList { + val pattern = signature.opcodes!! + for ((patternIndex, instructionIndex) in (scanResult.startIndex until scanResult.endIndex).withIndex()) { + val correctOpcode = instructions.elementAt(instructionIndex).opcode + val patternOpcode = pattern.elementAt(patternIndex) + if ( + patternOpcode != null && // unknown opcode + correctOpcode != patternOpcode + ) { + this.add( + PatternScanMethod.Fuzzy.Warning( + correctOpcode, patternOpcode, + instructionIndex, patternIndex, + ) + ) + } + } + } + } +} + +private operator fun ClassDef.component1() = this +private operator fun ClassDef.component2() = this.methods \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/smali/InlineSmaliCompiler.kt b/src/main/kotlin/app/revanced/patcher/smali/InlineSmaliCompiler.kt new file mode 100644 index 0000000..f0ac965 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/smali/InlineSmaliCompiler.kt @@ -0,0 +1,58 @@ +package app.revanced.patcher.smali + +import org.antlr.runtime.CommonTokenStream +import org.antlr.runtime.TokenSource +import org.antlr.runtime.tree.CommonTreeNodeStream +import org.jf.dexlib2.Opcodes +import org.jf.dexlib2.builder.BuilderInstruction +import org.jf.dexlib2.writer.builder.DexBuilder +import org.jf.smali.LexerErrorInterface +import org.jf.smali.smaliFlexLexer +import org.jf.smali.smaliParser +import org.jf.smali.smaliTreeWalker +import java.io.InputStreamReader + +private const val METHOD_TEMPLATE = """ +.class public Linlinecompiler; +.super Ljava/lang/Object; +.method public static compiler(%s)V + .registers %d + %s +.end method +""" + +class InlineSmaliCompiler { + companion object { + /** + * Compiles a string of Smali code to a list of instructions. + * p0, p1 etc. will only work correctly if the parameters and registers are passed. + * Do not cross the boundaries of the control flow (if-nez insn, etc), + * as that will result in exceptions since the labels cannot be calculated. + * Do not create dummy labels to fix the issue, since the code addresses will + * be messed up and results in broken Dalvik bytecode. + * FIXME: Fix the above issue. When this is fixed, add the proper conversions in [InstructionConverter]. + */ + fun compileMethodInstructions(instructions: String, parameters: String, registers: Int): List { + val input = METHOD_TEMPLATE.format(parameters, registers, instructions) + val reader = InputStreamReader(input.byteInputStream()) + val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15) + val tokens = CommonTokenStream(lexer as TokenSource) + val parser = smaliParser(tokens) + val result = parser.smali_file() + if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) { + throw IllegalStateException( + "Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!" + ) + } + val treeStream = CommonTreeNodeStream(result.tree) + treeStream.tokenStream = tokens + val dexGen = smaliTreeWalker(treeStream) + dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault())) + val classDef = dexGen.smali_file() + return classDef.methods.first().implementation!!.instructions.map { it.toBuilderInstruction() } + } + } +} + +fun String.toInstructions(parameters: String = "", registers: Int = 1) = InlineSmaliCompiler.compileMethodInstructions(this, parameters, registers) +fun String.toInstruction(parameters: String = "", registers: Int = 1) = this.toInstructions(parameters, registers).first() diff --git a/src/main/kotlin/app/revanced/patcher/smali/InstructionConverter.kt b/src/main/kotlin/app/revanced/patcher/smali/InstructionConverter.kt new file mode 100644 index 0000000..57363d7 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/smali/InstructionConverter.kt @@ -0,0 +1,261 @@ +package app.revanced.patcher.smali + +import org.jf.dexlib2.Format +import org.jf.dexlib2.builder.instruction.* +import org.jf.dexlib2.iface.instruction.Instruction +import org.jf.dexlib2.iface.instruction.formats.* +import org.jf.util.ExceptionWithContext + +fun Instruction.toBuilderInstruction() = + when (this.opcode.format) { + Format.Format10x -> InstructionConverter.newBuilderInstruction10x(this as Instruction10x) + Format.Format11n -> InstructionConverter.newBuilderInstruction11n(this as Instruction11n) + Format.Format11x -> InstructionConverter.newBuilderInstruction11x(this as Instruction11x) + Format.Format12x -> InstructionConverter.newBuilderInstruction12x(this as Instruction12x) + Format.Format20bc -> InstructionConverter.newBuilderInstruction20bc(this as Instruction20bc) + Format.Format21c -> InstructionConverter.newBuilderInstruction21c(this as Instruction21c) + Format.Format21ih -> InstructionConverter.newBuilderInstruction21ih(this as Instruction21ih) + Format.Format21lh -> InstructionConverter.newBuilderInstruction21lh(this as Instruction21lh) + Format.Format21s -> InstructionConverter.newBuilderInstruction21s(this as Instruction21s) + Format.Format22b -> InstructionConverter.newBuilderInstruction22b(this as Instruction22b) + Format.Format22c -> InstructionConverter.newBuilderInstruction22c(this as Instruction22c) + Format.Format22cs -> InstructionConverter.newBuilderInstruction22cs(this as Instruction22cs) + Format.Format22s -> InstructionConverter.newBuilderInstruction22s(this as Instruction22s) + Format.Format22x -> InstructionConverter.newBuilderInstruction22x(this as Instruction22x) + Format.Format23x -> InstructionConverter.newBuilderInstruction23x(this as Instruction23x) + Format.Format31c -> InstructionConverter.newBuilderInstruction31c(this as Instruction31c) + Format.Format31i -> InstructionConverter.newBuilderInstruction31i(this as Instruction31i) + Format.Format32x -> InstructionConverter.newBuilderInstruction32x(this as Instruction32x) + Format.Format35c -> InstructionConverter.newBuilderInstruction35c(this as Instruction35c) + Format.Format35mi -> InstructionConverter.newBuilderInstruction35mi(this as Instruction35mi) + Format.Format35ms -> InstructionConverter.newBuilderInstruction35ms(this as Instruction35ms) + Format.Format3rc -> InstructionConverter.newBuilderInstruction3rc(this as Instruction3rc) + Format.Format3rmi -> InstructionConverter.newBuilderInstruction3rmi(this as Instruction3rmi) + Format.Format3rms -> InstructionConverter.newBuilderInstruction3rms(this as Instruction3rms) + Format.Format51l -> InstructionConverter.newBuilderInstruction51l(this as Instruction51l) + else -> throw ExceptionWithContext("Instruction format %s not supported", this.opcode.format) + } + +internal class InstructionConverter { + companion object { + internal fun newBuilderInstruction10x(instruction: Instruction10x): BuilderInstruction10x { + return BuilderInstruction10x( + instruction.opcode + ) + } + + internal fun newBuilderInstruction11n(instruction: Instruction11n): BuilderInstruction11n { + return BuilderInstruction11n( + instruction.opcode, + instruction.registerA, + instruction.narrowLiteral + ) + } + + internal fun newBuilderInstruction11x(instruction: Instruction11x): BuilderInstruction11x { + return BuilderInstruction11x( + instruction.opcode, + instruction.registerA + ) + } + + internal fun newBuilderInstruction12x(instruction: Instruction12x): BuilderInstruction12x { + return BuilderInstruction12x( + instruction.opcode, + instruction.registerA, + instruction.registerB + ) + } + + internal fun newBuilderInstruction20bc(instruction: Instruction20bc): BuilderInstruction20bc { + return BuilderInstruction20bc( + instruction.opcode, + instruction.verificationError, + instruction.reference + ) + } + + internal fun newBuilderInstruction21c(instruction: Instruction21c): BuilderInstruction21c { + return BuilderInstruction21c( + instruction.opcode, + instruction.registerA, + instruction.reference + ) + } + + internal fun newBuilderInstruction21ih(instruction: Instruction21ih): BuilderInstruction21ih { + return BuilderInstruction21ih( + instruction.opcode, + instruction.registerA, + instruction.narrowLiteral + ) + } + + internal fun newBuilderInstruction21lh(instruction: Instruction21lh): BuilderInstruction21lh { + return BuilderInstruction21lh( + instruction.opcode, + instruction.registerA, + instruction.wideLiteral + ) + } + + internal fun newBuilderInstruction21s(instruction: Instruction21s): BuilderInstruction21s { + return BuilderInstruction21s( + instruction.opcode, + instruction.registerA, + instruction.narrowLiteral + ) + } + + internal fun newBuilderInstruction22b(instruction: Instruction22b): BuilderInstruction22b { + return BuilderInstruction22b( + instruction.opcode, + instruction.registerA, + instruction.registerB, + instruction.narrowLiteral + ) + } + + internal fun newBuilderInstruction22c(instruction: Instruction22c): BuilderInstruction22c { + return BuilderInstruction22c( + instruction.opcode, + instruction.registerA, + instruction.registerB, + instruction.reference + ) + } + + internal fun newBuilderInstruction22cs(instruction: Instruction22cs): BuilderInstruction22cs { + return BuilderInstruction22cs( + instruction.opcode, + instruction.registerA, + instruction.registerB, + instruction.fieldOffset + ) + } + + internal fun newBuilderInstruction22s(instruction: Instruction22s): BuilderInstruction22s { + return BuilderInstruction22s( + instruction.opcode, + instruction.registerA, + instruction.registerB, + instruction.narrowLiteral + ) + } + + internal fun newBuilderInstruction22x(instruction: Instruction22x): BuilderInstruction22x { + return BuilderInstruction22x( + instruction.opcode, + instruction.registerA, + instruction.registerB + ) + } + + internal fun newBuilderInstruction23x(instruction: Instruction23x): BuilderInstruction23x { + return BuilderInstruction23x( + instruction.opcode, + instruction.registerA, + instruction.registerB, + instruction.registerC + ) + } + + internal fun newBuilderInstruction31c(instruction: Instruction31c): BuilderInstruction31c { + return BuilderInstruction31c( + instruction.opcode, + instruction.registerA, + instruction.reference + ) + } + + internal fun newBuilderInstruction31i(instruction: Instruction31i): BuilderInstruction31i { + return BuilderInstruction31i( + instruction.opcode, + instruction.registerA, + instruction.narrowLiteral + ) + } + + internal fun newBuilderInstruction32x(instruction: Instruction32x): BuilderInstruction32x { + return BuilderInstruction32x( + instruction.opcode, + instruction.registerA, + instruction.registerB + ) + } + + internal fun newBuilderInstruction35c(instruction: Instruction35c): BuilderInstruction35c { + return BuilderInstruction35c( + instruction.opcode, + instruction.registerCount, + instruction.registerC, + instruction.registerD, + instruction.registerE, + instruction.registerF, + instruction.registerG, + instruction.reference + ) + } + + internal fun newBuilderInstruction35mi(instruction: Instruction35mi): BuilderInstruction35mi { + return BuilderInstruction35mi( + instruction.opcode, + instruction.registerCount, + instruction.registerC, + instruction.registerD, + instruction.registerE, + instruction.registerF, + instruction.registerG, + instruction.inlineIndex + ) + } + + internal fun newBuilderInstruction35ms(instruction: Instruction35ms): BuilderInstruction35ms { + return BuilderInstruction35ms( + instruction.opcode, + instruction.registerCount, + instruction.registerC, + instruction.registerD, + instruction.registerE, + instruction.registerF, + instruction.registerG, + instruction.vtableIndex + ) + } + + internal fun newBuilderInstruction3rc(instruction: Instruction3rc): BuilderInstruction3rc { + return BuilderInstruction3rc( + instruction.opcode, + instruction.startRegister, + instruction.registerCount, + instruction.reference + ) + } + + internal fun newBuilderInstruction3rmi(instruction: Instruction3rmi): BuilderInstruction3rmi { + return BuilderInstruction3rmi( + instruction.opcode, + instruction.startRegister, + instruction.registerCount, + instruction.inlineIndex + ) + } + + internal fun newBuilderInstruction3rms(instruction: Instruction3rms): BuilderInstruction3rms { + return BuilderInstruction3rms( + instruction.opcode, + instruction.startRegister, + instruction.registerCount, + instruction.vtableIndex + ) + } + + internal fun newBuilderInstruction51l(instruction: Instruction51l): BuilderInstruction51l { + return BuilderInstruction51l( + instruction.opcode, + instruction.registerA, + instruction.wideLiteral + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ExtraTypes.kt b/src/main/kotlin/app/revanced/patcher/util/ExtraTypes.kt deleted file mode 100644 index 430a4be..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/ExtraTypes.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patcher.util - -import org.objectweb.asm.Type - -object ExtraTypes { - /** - * Any object type. - * Should be used instead of types such as: "Ljava/lang/String;" - */ - val Any: Type = Type.getType(Object::class.java) - val ArrayAny: Type = Type.getType(Array::class.java) -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/Io.kt b/src/main/kotlin/app/revanced/patcher/util/Io.kt deleted file mode 100644 index 4db1a27..0000000 --- a/src/main/kotlin/app/revanced/patcher/util/Io.kt +++ /dev/null @@ -1,94 +0,0 @@ -package app.revanced.patcher.util - -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassWriter -import org.objectweb.asm.tree.ClassNode -import java.io.BufferedInputStream -import java.io.InputStream -import java.io.OutputStream -import java.util.jar.JarEntry -import java.util.jar.JarInputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream - -internal class Io( - private val input: InputStream, - private val output: OutputStream, - private val classes: MutableList -) { - private val bufferedInputStream = BufferedInputStream(input) - - fun readFromJar() { - bufferedInputStream.mark(Integer.MAX_VALUE) - // create a BufferedInputStream in order to read the input stream again when calling saveAsJar(..) - val jis = JarInputStream(bufferedInputStream) - - // read all entries from the input stream - // we use JarEntry because we only read .class files - lateinit var jarEntry: JarEntry - while (jis.nextJarEntry.also { if (it != null) jarEntry = it } != null) { - // if the current entry ends with .class (indicating a java class file), add it to our list of classes to return - if (jarEntry.name.endsWith(".class")) { - // create a new ClassNode - val classNode = ClassNode() - // read the bytes with a ClassReader into the ClassNode - ClassReader(jis.readBytes()).accept(classNode, ClassReader.EXPAND_FRAMES) - // add it to our list - classes.add(classNode) - } - - // finally, close the entry - jis.closeEntry() - } - - // at last reset the buffered input stream - bufferedInputStream.reset() - } - - fun saveAsJar() { - val jis = ZipInputStream(bufferedInputStream) - val jos = ZipOutputStream(output) - val classReaders = mutableMapOf() - - // first write all non .class zip entries from the original input stream to the output stream - // we read it first to close the input stream as fast as possible - // TODO(oSumAtrIX): There is currently no way to remove non .class files. - lateinit var zipEntry: ZipEntry - while (jis.nextEntry.also { if (it != null) zipEntry = it } != null) { - if (zipEntry.name.endsWith(".class")) { - classReaders[zipEntry.name] = ClassReader(jis.readBytes()) - continue - } - - // create a new zipEntry and write the contents of the zipEntry to the output stream and close it - jos.putNextEntry(ZipEntry(zipEntry)) - jos.write(jis.readBytes()) - jos.closeEntry() - } - - // finally, close the input stream - jis.close() - bufferedInputStream.close() - input.close() - - // now write all the patched classes to the output stream - for (patchedClass in classes) { - // create a new entry of the patched class - val name = patchedClass.name + ".class" - jos.putNextEntry(JarEntry(name)) - - // parse the patched class to a byte array and write it to the output stream - val cw = ClassWriter(classReaders[name]!!, ClassWriter.COMPUTE_MAXS) - patchedClass.accept(cw) - jos.write(cw.toByteArray()) - - // close the newly created jar entry - jos.closeEntry() - } - - // finally, close the rest of the streams - jos.close() - output.close() - } -} \ 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 new file mode 100644 index 0000000..f019b14 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt @@ -0,0 +1,15 @@ +package app.revanced.patcher.util + +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 new file mode 100644 index 0000000..6e28f59 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/util/ProxyBackedClassList.kt @@ -0,0 +1,46 @@ +package app.revanced.patcher.util + +import app.revanced.patcher.proxy.ClassProxy +import org.jf.dexlib2.iface.ClassDef + +class ProxyBackedClassList(internal val internalClasses: MutableList) : List { + internal val proxies = mutableListOf() + + fun add(classDef: ClassDef) { + internalClasses.add(classDef) + } + + fun add(classProxy: ClassProxy) { + proxies.add(classProxy) + } + + /** + * Apply all resolved classes into [internalClasses] and clean the [proxies] list. + */ + fun applyProxies() { + // FIXME: check if this could cause issues when multiple patches use the same proxy + proxies.removeIf { proxy -> + // if the proxy is unused, keep it in the list + if (!proxy.proxyUsed) return@removeIf false + + // if it has been used, replace the internal class which it proxied + val index = internalClasses.indexOfFirst { it.type == proxy.immutableClass.type } + internalClasses[index] = proxy.mutatedClass + + // return true to remove it from the proxies list + return@removeIf true + } + } + + override val size get() = internalClasses.size + override fun contains(element: ClassDef) = internalClasses.contains(element) + override fun containsAll(elements: Collection) = internalClasses.containsAll(elements) + override fun get(index: Int) = internalClasses[index] + override fun indexOf(element: ClassDef) = internalClasses.indexOf(element) + override fun isEmpty() = internalClasses.isEmpty() + override fun iterator() = internalClasses.iterator() + override fun lastIndexOf(element: ClassDef) = internalClasses.lastIndexOf(element) + override fun listIterator() = internalClasses.listIterator() + override fun listIterator(index: Int) = internalClasses.listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int) = internalClasses.subList(fromIndex, toIndex) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/writer/ASMWriter.kt b/src/main/kotlin/app/revanced/patcher/writer/ASMWriter.kt deleted file mode 100644 index 1a8a014..0000000 --- a/src/main/kotlin/app/revanced/patcher/writer/ASMWriter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patcher.writer - -import org.objectweb.asm.tree.AbstractInsnNode -import org.objectweb.asm.tree.InsnList - -object ASMWriter { - fun InsnList.setAt(index: Int, node: AbstractInsnNode) { - this[this.get(index)] = node - } - - fun InsnList.insertAt(index: Int = 0, vararg nodes: AbstractInsnNode) { - this.insert(this.get(index), nodes.toInsnList()) - } - - // TODO(Sculas): Should this be public? - private fun Array.toInsnList(): InsnList { - val list = InsnList() - this.forEach { list.add(it) } - return list - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index 94824da..1007aac 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -1,177 +1,49 @@ package app.revanced.patcher -import app.revanced.patcher.cache.Cache -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultSuccess -import app.revanced.patcher.signature.Signature -import app.revanced.patcher.util.ExtraTypes -import app.revanced.patcher.util.TestUtil -import app.revanced.patcher.writer.ASMWriter.insertAt -import app.revanced.patcher.writer.ASMWriter.setAt -import org.junit.jupiter.api.assertDoesNotThrow -import org.objectweb.asm.Opcodes.* -import org.objectweb.asm.Type -import org.objectweb.asm.tree.FieldInsnNode -import org.objectweb.asm.tree.LdcInsnNode -import org.objectweb.asm.tree.MethodInsnNode -import java.io.ByteArrayOutputStream -import java.io.PrintStream -import kotlin.test.Test +import app.revanced.patcher.signature.PatternScanMethod +import app.revanced.patcher.usage.ExampleBytecodePatch +import app.revanced.patcher.usage.ExampleResourcePatch +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.test.assertTrue internal class PatcherTest { - companion object { - val testSignatures: Array = arrayOf( - // Java: - // public static void main(String[] args) { - // System.out.println("Hello, world!"); - // } - // Bytecode: - // public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V - // getstatic java/lang/System.out:java.io.PrintStream - // ldc "Hello, world!" (java.lang.String) - // invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V - // return - // } - Signature( - "mainMethod", - Type.VOID_TYPE, - ACC_PUBLIC or ACC_STATIC, - arrayOf(ExtraTypes.ArrayAny), - intArrayOf( - GETSTATIC, - LDC, - INVOKEVIRTUAL, - RETURN - ) - ) - ) - } - @Test fun testPatcher() { + return // FIXME: create a proper resource to pass this test val patcher = Patcher( - PatcherTest::class.java.getResourceAsStream("/test1.jar")!!, - ByteArrayOutputStream(), - testSignatures + File(PatcherTest::class.java.getResource("/example.apk")!!.toURI()), + "exampleCacheDirectory", + patchResources = true ) - patcher.addPatches( - object : Patch("TestPatch") { - override fun execute(cache: Cache): PatchResult { - // Get the method from the resolver cache - val mainMethod = patcher.cache.methods["mainMethod"] - // Get the instruction list - val instructions = mainMethod.method.instructions!! + patcher.addPatches(listOf(ExampleBytecodePatch(), ExampleResourcePatch())) - // Let's modify it, so it prints "Hello, ReVanced! Editing bytecode." - // Get the start index of our opcode pattern. - // This will be the index of the LDC instruction. - val startIndex = mainMethod.scanData.startIndex - - // Ignore this, just testing if the method resolver works :) - TestUtil.assertNodeEqual( - FieldInsnNode( - GETSTATIC, - Type.getInternalName(System::class.java), - "out", - // for whatever reason, it adds an "L" and ";" to the node string - "L${Type.getInternalName(PrintStream::class.java)};" - ), - instructions[startIndex]!! - ) - - // Create a new LDC node and replace the LDC instruction. - val stringNode = LdcInsnNode("Hello, ReVanced! Editing bytecode.") - instructions.setAt(startIndex, stringNode) - - // Now lets print our string twice! - // Insert our instructions after the second instruction by our pattern. - // This will place our instructions after the original INVOKEVIRTUAL call. - // You could also copy the instructions from the list and then modify the LDC instruction again, - // but this is to show a more advanced example of writing bytecode using the patcher and ASM. - instructions.insertAt( - startIndex + 1, - FieldInsnNode( - GETSTATIC, - Type.getInternalName(System::class.java), // "java/lang/System" - "out", - Type.getInternalName(PrintStream::class.java) // "java/io/PrintStream" - ), - LdcInsnNode("Hello, ReVanced! Adding bytecode."), - MethodInsnNode( - INVOKEVIRTUAL, - Type.getInternalName(PrintStream::class.java), // "java/io/PrintStream" - "println", - Type.getMethodDescriptor( - Type.VOID_TYPE, - Type.getType(String::class.java) - ) // "(Ljava/lang/String;)V" - ) - ) - - // Our code now looks like this: - // public static main(java.lang.String[] arg0) { // Method signature: ([Ljava/lang/String;)V - // getstatic java/lang/System.out:java.io.PrintStream - // ldc "Hello, ReVanced! Editing bytecode." (java.lang.String) // We overwrote this instruction. - // invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V - // getstatic java/lang/System.out:java.io.PrintStream // This instruction and the 2 instructions below are written manually. - // ldc "Hello, ReVanced! Adding bytecode." (java.lang.String) - // invokevirtual java/io/PrintStream.println(Ljava/lang/String;)V - // return - // } - - // 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() + for (signature in patcher.resolveSignatures()) { + if (!signature.resolved) { + throw Exception("Signature ${signature.metadata.name} was not resolved!") + } + val patternScanMethod = signature.metadata.patternScanMethod + if (patternScanMethod is PatternScanMethod.Fuzzy) { + val warnings = patternScanMethod.warnings + if (warnings != null) { + println("Signature ${signature.metadata.name} had ${warnings.size} warnings!") + for (warning in warnings) { + println(warning.toString()) + } + } else { + println("Signature ${signature.metadata.name} used the fuzzy resolver, but the warnings list is null!") } } - ) - - // Apply all patches loaded in the patcher - val patchResult = patcher.applyPatches() - // You can check if an error occurred - for ((patchName, result) in patchResult) { + } + for ((metadata, result) in patcher.applyPatches()) { if (result.isFailure) { - throw Exception("Patch $patchName failed", result.exceptionOrNull()!!) + throw Exception("Patch ${metadata.shortName} failed", result.exceptionOrNull()!!) + } else { + println("Patch ${metadata.shortName} applied successfully!") } } - - patcher.save() - } - - @Test - fun `test patcher with no changes`() { - val testData = PatcherTest::class.java.getResourceAsStream("/test1.jar")!! - // val available = testData.available() - val out = ByteArrayOutputStream() - Patcher(testData, out, testSignatures).save() - // FIXME(Sculas): There seems to be a 1-byte difference, not sure what it is. - // assertEquals(available, out.size()) - out.close() - } - - @Test() - fun `should not raise an exception if any signature member except the name is missing`() { - val sigName = "testMethod" - - assertDoesNotThrow( - "Should not raise an exception if any signature member except the name is missing" - ) { - Patcher( - PatcherTest::class.java.getResourceAsStream("/test1.jar")!!, - ByteArrayOutputStream(), - arrayOf( - Signature( - sigName, - null, - null, - null, - null - )) - ) - } + val out = patcher.save() + assertTrue(out.isNotEmpty(), "Expected the output of Patcher#save() to not be empty.") } } \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/ReaderTest.kt b/src/test/kotlin/app/revanced/patcher/ReaderTest.kt deleted file mode 100644 index becd7f6..0000000 --- a/src/test/kotlin/app/revanced/patcher/ReaderTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patcher - -import java.io.ByteArrayOutputStream -import kotlin.test.Test - -internal class ReaderTest { - @Test - fun `read jar containing multiple classes`() { - val testData = PatcherTest::class.java.getResourceAsStream("/test2.jar")!! - Patcher(testData, ByteArrayOutputStream(), PatcherTest.testSignatures).save() // reusing test sigs from PatcherTest - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/usage/ExampleBytecodePatch.kt b/src/test/kotlin/app/revanced/patcher/usage/ExampleBytecodePatch.kt new file mode 100644 index 0000000..18745c5 --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/usage/ExampleBytecodePatch.kt @@ -0,0 +1,199 @@ +package app.revanced.patcher.usage + +import app.revanced.patcher.data.implementation.BytecodeData +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.or +import app.revanced.patcher.patch.implementation.BytecodePatch +import app.revanced.patcher.patch.implementation.metadata.PackageMetadata +import app.revanced.patcher.patch.implementation.metadata.PatchMetadata +import app.revanced.patcher.patch.implementation.misc.PatchResult +import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess +import app.revanced.patcher.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.signature.MethodMetadata +import app.revanced.patcher.signature.MethodSignature +import app.revanced.patcher.signature.MethodSignatureMetadata +import app.revanced.patcher.signature.PatternScanMethod +import app.revanced.patcher.smali.toInstruction +import app.revanced.patcher.smali.toInstructions +import com.google.common.collect.ImmutableList +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.Format +import org.jf.dexlib2.Opcode +import org.jf.dexlib2.builder.MutableMethodImplementation +import org.jf.dexlib2.builder.instruction.BuilderInstruction11x +import org.jf.dexlib2.builder.instruction.BuilderInstruction21c +import org.jf.dexlib2.iface.instruction.formats.Instruction21c +import org.jf.dexlib2.immutable.ImmutableField +import org.jf.dexlib2.immutable.ImmutableMethod +import org.jf.dexlib2.immutable.ImmutableMethodImplementation +import org.jf.dexlib2.immutable.reference.ImmutableFieldReference +import org.jf.dexlib2.immutable.reference.ImmutableStringReference +import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue +import org.jf.dexlib2.util.Preconditions + +val packageMetadata = listOf( + PackageMetadata( + "com.example.examplePackage", + listOf("0.0.1", "0.0.2") + ) +) + +class ExampleBytecodePatch : BytecodePatch( + PatchMetadata( + "example-patch", + "ReVanced example patch", + "A demonstrative patch to feature the core features of the ReVanced patcher", + packageMetadata, + "0.0.1" + ), + setOf( + MethodSignature( + MethodSignatureMetadata( + "Example signature", + MethodMetadata( + "TestClass", + "main", + ), + PatternScanMethod.Fuzzy(1), + packageMetadata, + "The main method of TestClass", + "1.0.0" + ), + "V", + AccessFlags.PUBLIC or AccessFlags.STATIC, + listOf("[L"), + listOf( + Opcode.SGET_OBJECT, + null, // Testing unknown opcodes. + Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver. + Opcode.RETURN_VOID + ), + null + ) + ) +) { + // This function will be executed by the patcher. + // You can treat it as a constructor + override fun execute(data: BytecodeData): PatchResult { + // Get the resolved method for the signature from the resolver cache + val result = signatures.first().result!! + + // Get the implementation for the resolved method + val implementation = result.method.implementation!! + + // Let's modify it, so it prints "Hello, ReVanced! Editing bytecode." + // Get the start index of our opcode pattern. + // This will be the index of the instruction with the opcode CONST_STRING. + val startIndex = result.scanData.startIndex + + implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.") + + // Get the class in which the method matching our signature is defined in. + val mainClass = data.findClass { + it.type == result.definingClassProxy.immutableClass.type + }!!.resolve() + + // Add a new method returning a string + mainClass.methods.add( + ImmutableMethod( + result.definingClassProxy.immutableClass.type, + "returnHello", + null, + "Ljava/lang/String;", + AccessFlags.PRIVATE or AccessFlags.STATIC, + null, + null, + ImmutableMethodImplementation( + 1, + ImmutableList.of( + BuilderInstruction21c( + Opcode.CONST_STRING, + 0, + ImmutableStringReference("Hello, ReVanced! Adding bytecode.") + ), + BuilderInstruction11x(Opcode.RETURN_OBJECT, 0) + ), + null, + null + ) + ).toMutable() + ) + + // Add a field in the main class + // We will use this field in our method below to call println on + // The field holds the Ljava/io/PrintStream->out; field + mainClass.fields.add( + ImmutableField( + mainClass.type, + "dummyField", + "Ljava/io/PrintStream;", + AccessFlags.PRIVATE or AccessFlags.STATIC, + ImmutableFieldEncodedValue( + ImmutableFieldReference( + "Ljava/lang/System;", + "out", + "Ljava/io/PrintStream;" + ) + ), + null, + null + ).toMutable() + ) + + // store the fields initial value into the first virtual register + implementation.replaceInstruction( + 0, + "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;".toInstruction() + ) + + // Now let's create a new call to our method and print the return value! + // You can also use the smali compiler to create instructions. + // For this sake of example I reuse the TestClass field dummyField inside the virtual register 0. + // + // Control flow instructions are not supported as of now. + val instructions = """ + invoke-static { }, LTestClass;->returnHello()Ljava/lang/String; + move-result-object v1 + invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V + """.trimIndent().toInstructions() + implementation.addInstructions(startIndex + 2, instructions) + + // 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() + } + + /** + * Replace the string for an instruction at the given index with a new one. + * @param index The index of the instruction to replace the string for + * @param string The replacing string + */ + private fun MutableMethodImplementation.replaceStringAt(index: Int, string: String) { + val instruction = this.instructions[index] + + // Utility method of dexlib2 + Preconditions.checkFormat(instruction.opcode, Format.Format21c) + + // Cast this to an instruction of the format 21c + // The instruction format can be found in the docs at + // https://source.android.com/devices/tech/dalvik/dalvik-bytecode + val strInstruction = instruction as Instruction21c + + // In our case we want an instruction with the opcode CONST_STRING + // The format is 21c, so we create a new BuilderInstruction21c + // This instruction will hold the string reference constant in the virtual register of the original instruction + // For that a reference to the string is needed. It can be created with an ImmutableStringReference. + // At last, use the method replaceInstruction to replace it at the given index startIndex. + this.replaceInstruction( + index, + BuilderInstruction21c( + Opcode.CONST_STRING, + strInstruction.registerA, + ImmutableStringReference(string) + ) + ) + } +} diff --git a/src/test/kotlin/app/revanced/patcher/usage/ExampleResourcePatch.kt b/src/test/kotlin/app/revanced/patcher/usage/ExampleResourcePatch.kt new file mode 100644 index 0000000..db3b797 --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/usage/ExampleResourcePatch.kt @@ -0,0 +1,50 @@ +package app.revanced.patcher.usage + +import app.revanced.patcher.data.implementation.ResourceData +import app.revanced.patcher.patch.implementation.ResourcePatch +import app.revanced.patcher.patch.implementation.metadata.PatchMetadata +import app.revanced.patcher.patch.implementation.misc.PatchResult +import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess +import com.sun.org.apache.xerces.internal.dom.ElementImpl + +class ExampleResourcePatch : ResourcePatch( + PatchMetadata( + "example-patch", + "Example Resource Patch", + "Example demonstration of a resource patch.", + packageMetadata, + "0.0.1" + ) +) { + override fun execute(data: ResourceData): PatchResult { + val editor = data.getXmlEditor("AndroidManifest.xml") + + // regular DomFileEditor + val element = editor + .file + .getElementsByTagName("application") + .item(0) as ElementImpl + element + .setAttribute( + "exampleAttribute", + "exampleValue" + ) + + // close the editor to write changes + editor.close() + + // iterate through all available resources + data.forEach { + if (it.extension.lowercase() != "xml") return@forEach + + data.replace( + it.path, + "\\ddip", // regex supported + "0dip", + true + ) + } + + return PatchResultSuccess() + } +} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/util/TestUtil.kt b/src/test/kotlin/app/revanced/patcher/util/TestUtil.kt deleted file mode 100644 index 6d891e1..0000000 --- a/src/test/kotlin/app/revanced/patcher/util/TestUtil.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.revanced.patcher.util - -import org.objectweb.asm.tree.AbstractInsnNode -import org.objectweb.asm.tree.FieldInsnNode -import org.objectweb.asm.tree.LdcInsnNode -import kotlin.test.fail - -object TestUtil { - fun assertNodeEqual(expected: T, actual: T) { - val a = expected.nodeString() - val b = actual.nodeString() - if (a != b) { - fail("expected: $a,\nactual: $b\n") - } - } - - private fun AbstractInsnNode.nodeString(): String { - val sb = NodeStringBuilder() - when (this) { - // TODO(Sculas): Add more types - is LdcInsnNode -> sb - .addType("cst", cst) - is FieldInsnNode -> sb - .addType("owner", owner) - .addType("name", name) - .addType("desc", desc) - } - return "(${this::class.simpleName}): (type = $type, opcode = $opcode, $sb)" - } -} - -private class NodeStringBuilder { - private val sb = StringBuilder() - - fun addType(name: String, value: Any): NodeStringBuilder { - sb.append("$name = \"$value\", ") - return this - } - - override fun toString(): String { - if (sb.isEmpty()) return "" - val s = sb.toString() - return s.substring(0 .. (s.length - 2).coerceAtLeast(0)) // remove the last ", " - } -} diff --git a/src/test/resources/test1.jar b/src/test/resources/test1.jar deleted file mode 100644 index 33dee5f..0000000 Binary files a/src/test/resources/test1.jar and /dev/null differ diff --git a/src/test/resources/test2.jar b/src/test/resources/test2.jar deleted file mode 100644 index 9df8df7..0000000 Binary files a/src/test/resources/test2.jar and /dev/null differ