Skip to content

Commit

Permalink
add: resource patcher
Browse files Browse the repository at this point in the history
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
  • Loading branch information
oSumAtrIX committed Jun 5, 2022
1 parent c459beb commit 99319e6
Show file tree
Hide file tree
Showing 17 changed files with 304 additions and 74 deletions.
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -66,4 +67,4 @@ publishing {
from(components["java"])
}
}
}
}
100 changes: 76 additions & 24 deletions src/main/kotlin/app/revanced/patcher/Patcher.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}

/**
Expand All @@ -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)
}
}
}
Expand All @@ -70,15 +104,22 @@ 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 {
return this@Patcher.opcodes
}
}

// 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
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions src/main/kotlin/app/revanced/patcher/data/PatcherData.kt
Original file line number Diff line number Diff line change
@@ -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))
}
9 changes: 9 additions & 0 deletions src/main/kotlin/app/revanced/patcher/data/base/Data.kt
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,21 +31,22 @@ 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

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<String, SignatureResolverResult>() {
override fun get(key: String): SignatureResolverResult {
return super.get(key) ?: throw MethodNotFoundException("Method $key was not found in the method cache")
Expand All @@ -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)
}

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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()))
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
) {
/**
Expand All @@ -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 ->
Expand Down

0 comments on commit 99319e6

Please sign in to comment.