add: resource patcher

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
oSumAtrIX 2022-05-04 23:46:04 +02:00
parent c459beb5f8
commit 99319e63da
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
17 changed files with 304 additions and 74 deletions

View File

@ -24,6 +24,7 @@ repositories {
dependencies {
implementation(kotlin("stdlib"))
api("org.apktool:apktool-lib:2.6.1")
api("app.revanced:multidexlib2:2.5.2.r2")
api("org.smali:smali:2.5.2")

View File

@ -1,11 +1,19 @@
package app.revanced.patcher
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchMetadata
import app.revanced.patcher.patch.PatchResultSuccess
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.ApkDecoder
import brut.directory.ExtFile
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
@ -18,20 +26,46 @@ import java.io.File
val NAMER = BasicDexFileNamer()
/**
* ReVanced Patcher.
* @param input The input file (an apk or any other multi dex container).
* 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(
input: File,
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
) {
val packageVersion: String
val packageName: String
private val patcherData: PatcherData
private val opcodes: Opcodes
private var signaturesResolved = false
private val androlib = Androlib()
init {
val dexFile = MultiDexIO.readDexFile(true, input, NAMER, null, null)
// FIXME: only use androlib instead of ApkDecoder which is currently a temporal solution
val decoder = ApkDecoder(androlib)
decoder.setApkFile(inputFile)
decoder.setDecodeSources(ApkDecoder.DECODE_SOURCES_NONE)
decoder.setForceDelete(true)
// decode resources to cache directory
decoder.setOutDir(File(resourceCacheDirectory))
decoder.decode()
// get package info
packageName = decoder.resTable.packageOriginal
packageVersion = decoder.resTable.versionInfo.versionName
// read dex files
val dexFile = MultiDexIO.readDexFile(true, inputFile, NAMER, null, null)
opcodes = dexFile.opcodes
patcherData = PatcherData(dexFile.classes.toMutableList())
// save to patcher data
patcherData = PatcherData(dexFile.classes.toMutableList(), resourceCacheDirectory)
}
/**
@ -48,18 +82,18 @@ class Patcher(
for (file in files) {
val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null)
for (classDef in dexFile.classes) {
val e = patcherData.classes.internalClasses.findIndexed { it.type == classDef.type }
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.classes.internalClasses[idx] = classDef
patcherData.bytecodeData.classes.internalClasses[idx] = classDef
}
continue
}
patcherData.classes.internalClasses.add(classDef)
patcherData.bytecodeData.classes.internalClasses.add(classDef)
}
}
}
@ -70,8 +104,8 @@ class Patcher(
fun save(): Map<String, MemoryDataStore> {
val newDexFile = object : DexFile {
override fun getClasses(): Set<ClassDef> {
patcherData.classes.applyProxies()
return ListBackedSet(patcherData.classes.internalClasses)
patcherData.bytecodeData.classes.applyProxies()
return ListBackedSet(patcherData.bytecodeData.classes.internalClasses)
}
override fun getOpcodes(): Opcodes {
@ -79,6 +113,13 @@ class Patcher(
}
}
// build modified resources
if (patchResources) {
val extDir = ExtFile(resourceCacheDirectory)
androlib.buildResources(extDir, androlib.readMetaFile(extDir).usesFramework)
}
// write dex modified files
val output = mutableMapOf<String, MemoryDataStore>()
MultiDexIO.writeDexFile(
true, -1, // core count
@ -93,24 +134,25 @@ class Patcher(
* Add a patch to the patcher.
* @param patches The patches to add.
*/
fun addPatches(patches: Iterable<Patch>) {
fun addPatches(patches: Iterable<Patch<Data>>) {
patcherData.patches.addAll(patches)
}
/**
* Resolves all signatures.
* @throws IllegalStateException if signatures have already been resolved.
*/
fun resolveSignatures(): List<MethodSignature> {
if (signaturesResolved) {
throw IllegalStateException("Signatures have already been resolved.")
val signatures = buildList {
for (patch in patcherData.patches) {
if (patch !is BytecodePatch) continue
this.addAll(patch.signatures)
}
}
if (signatures.isEmpty()) {
return emptyList()
}
val signatures = patcherData.patches.flatMap { it.signatures }
if (signatures.isEmpty()) return emptyList()
SignatureResolver(patcherData.classes.internalClasses, signatures).resolve(patcherData)
SignatureResolver(patcherData.bytecodeData.classes.internalClasses, signatures).resolve(patcherData)
signaturesResolved = true
return signatures
}
@ -126,14 +168,24 @@ class Patcher(
stopOnError: Boolean = false,
callback: (String) -> Unit = {}
): Map<PatchMetadata, Result<PatchResultSuccess>> {
if (!signaturesResolved && patcherData.patches.isNotEmpty()) {
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<PatchResultSuccess> = try {
val pr = patch.execute(patcherData)
val data = if (resourcePatch) {
patcherData.resourceData
} else {
patcherData.bytecodeData
}
val pr = patch.execute(data)
if (pr.isSuccess()) {
Result.success(pr.success()!!)
} else {

View File

@ -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<ClassDef>,
val resourceCacheDirectory: String
) {
internal val patches = mutableListOf<Patch<Data>>()
internal val bytecodeData = BytecodeData(patches, internalClasses)
internal val resourceData = ResourceData(File(resourceCacheDirectory))
}

View File

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

View File

@ -1,18 +1,22 @@
package app.revanced.patcher
package app.revanced.patcher.data.implementation
import app.revanced.patcher.data.base.Data
import app.revanced.patcher.methodWalker.MethodWalker
import app.revanced.patcher.patch.Patch
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 PatcherData(
internalClasses: MutableList<ClassDef>,
) {
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<Patch<Data>>,
internalClasses: MutableList<ClassDef>
) : Data {
val classes = ProxyBackedClassList(internalClasses)
internal val patches = mutableListOf<Patch>()
/**
* Find a class by a given class name
@ -27,6 +31,7 @@ class PatcherData(
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
@ -34,7 +39,6 @@ class PatcherData(
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)
@ -42,6 +46,7 @@ class PatcherData(
}
}
class MethodMap : LinkedHashMap<String, SignatureResolverResult>() {
override fun get(key: String): SignatureResolverResult {
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
@ -59,7 +64,7 @@ internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T?
return null
}
fun PatcherData.toMethodWalker(startMethod: Method): MethodWalker {
fun BytecodeData.toMethodWalker(startMethod: Method): MethodWalker {
return MethodWalker(this, startMethod)
}
@ -72,7 +77,7 @@ internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair
return null
}
fun PatcherData.proxy(classDef: ClassDef): ClassProxy {
fun BytecodeData.proxy(classDef: ClassDef): ClassProxy {
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) {
proxy = ClassProxy(classDef)

View File

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

View File

@ -1,7 +1,7 @@
package app.revanced.patcher.methodWalker
import app.revanced.patcher.MethodNotFoundException
import app.revanced.patcher.PatcherData
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
@ -12,11 +12,11 @@ import org.jf.dexlib2.util.Preconditions
/**
* Find a method from another method via instruction offsets.
* @param patcherData The patcherData to use when resolving the next method reference.
* @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 patcherData: PatcherData,
private val bytecodeData: BytecodeData,
private var currentMethod: Method
) {
/**
@ -40,7 +40,7 @@ class MethodWalker internal constructor(
Preconditions.checkFormat(instruction.opcode, Format.Format35c)
val newMethod = (instruction as Instruction35c).reference as MethodReference
val proxy = patcherData.findClass(newMethod.definingClass)!!
val proxy = bytecodeData.findClass(newMethod.definingClass)!!
val methods = if (walkMutable) proxy.resolve().methods else proxy.immutableClass.methods
currentMethod = methods.first { it ->

View File

@ -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<out T : Data>(
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
}

View File

@ -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<MethodSignature>
) : Patch<BytecodeData>(metadata)

View File

@ -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<ResourceData>(metadata)

View File

@ -1,23 +1,6 @@
package app.revanced.patcher.patch
package app.revanced.patcher.patch.implementation.metadata
import app.revanced.patcher.PatcherData
import app.revanced.patcher.signature.MethodSignature
/**
* Patch for the Patcher.
* @param metadata [PatchMetadata] for the patch.
* @param signatures A list of [MethodSignature] this patch relies on.
*/
abstract class Patch(
val metadata: PatchMetadata,
val signatures: Iterable<MethodSignature>
) {
/**
* The main function of the [Patch] which the patcher will call.
*/
abstract fun execute(patcherData: PatcherData): PatchResult
}
import app.revanced.patcher.patch.base.Patch
/**
* Metadata about a [Patch].

View File

@ -1,4 +1,4 @@
package app.revanced.patcher.patch
package app.revanced.patcher.patch.implementation.misc
interface PatchResult {
fun error(): PatchResultError? {

View File

@ -1,7 +1,7 @@
package app.revanced.patcher.signature
import app.revanced.patcher.MethodNotFoundException
import app.revanced.patcher.patch.PackageMetadata
import app.revanced.patcher.data.implementation.MethodNotFoundException
import app.revanced.patcher.patch.implementation.metadata.PackageMetadata
import org.jf.dexlib2.Opcode
/**
@ -36,7 +36,8 @@ class MethodSignature(
var resolved = false
try {
resolved = result != null
} catch (_: Exception) {}
} catch (_: Exception) {
}
return resolved
}
}

View File

@ -1,8 +1,8 @@
package app.revanced.patcher.signature.resolver
import app.revanced.patcher.PatcherData
import app.revanced.patcher.data.PatcherData
import app.revanced.patcher.data.implementation.proxy
import app.revanced.patcher.extensions.parametersEqual
import app.revanced.patcher.proxy
import app.revanced.patcher.proxy.ClassProxy
import app.revanced.patcher.signature.MethodSignature
import app.revanced.patcher.signature.PatternScanMethod
@ -26,7 +26,7 @@ internal class SignatureResolver(
val patternScanData = compareSignatureToMethod(signature, method) ?: continue
// create class proxy, in case a patch needs mutability
val classProxy = patcherData.proxy(classDef)
val classProxy = patcherData.bytecodeData.proxy(classDef)
signature.result = SignatureResolverResult(
classProxy,
patternScanData,

View File

@ -1,7 +1,8 @@
package app.revanced.patcher
import app.revanced.patcher.signature.PatternScanMethod
import app.revanced.patcher.usage.ExamplePatch
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
@ -9,8 +10,14 @@ import kotlin.test.assertTrue
internal class PatcherTest {
@Test
fun testPatcher() {
val patcher = Patcher(File(PatcherTest::class.java.getResource("/test1.dex")!!.toURI()))
patcher.addPatches(listOf(ExamplePatch()))
val patcher = Patcher(
File(PatcherTest::class.java.getResource("/test1.dex")!!.toURI()),
"exampleCacheDirectory",
patchResources = true
)
patcher.addPatches(listOf(ExampleBytecodePatch(), ExampleResourcePatch()))
for (signature in patcher.resolveSignatures()) {
if (!signature.resolved) {
throw Exception("Signature ${signature.metadata.name} was not resolved!")

View File

@ -1,9 +1,13 @@
package app.revanced.patcher.usage
import app.revanced.patcher.PatcherData
import app.revanced.patcher.data.implementation.BytecodeData
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.*
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
@ -35,7 +39,7 @@ val packageMetadata = listOf(
)
)
class ExamplePatch : Patch(
class ExampleBytecodePatch : BytecodePatch(
PatchMetadata(
"example-patch",
"ReVanced example patch",
@ -71,7 +75,7 @@ class ExamplePatch : Patch(
) {
// This function will be executed by the patcher.
// You can treat it as a constructor
override fun execute(patcherData: PatcherData): PatchResult {
override fun execute(data: BytecodeData): PatchResult {
// Get the resolved method for the signature from the resolver cache
val result = signatures.first().result!!
@ -86,7 +90,7 @@ class ExamplePatch : Patch(
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
// Get the class in which the method matching our signature is defined in.
val mainClass = patcherData.findClass {
val mainClass = data.findClass {
it.type == result.definingClassProxy.immutableClass.type
}!!.resolve()

View File

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