fix: resource patcher

This commit is contained in:
oSumAtrIX 2022-06-11 06:36:13 +02:00
parent 1ce6098cad
commit 31815ca9ea
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
8 changed files with 165 additions and 62 deletions

View File

@ -1,5 +1,6 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.PackageMetadata
import app.revanced.patcher.data.PatcherData import app.revanced.patcher.data.PatcherData
import app.revanced.patcher.data.base.Data import app.revanced.patcher.data.base.Data
import app.revanced.patcher.data.implementation.findIndexed import app.revanced.patcher.data.implementation.findIndexed
@ -16,11 +17,13 @@ import app.revanced.patcher.signature.implementation.method.resolver.MethodSigna
import app.revanced.patcher.util.ListBackedSet import app.revanced.patcher.util.ListBackedSet
import brut.androlib.Androlib import brut.androlib.Androlib
import brut.androlib.meta.UsesFramework import brut.androlib.meta.UsesFramework
import brut.androlib.options.BuildOptions
import brut.androlib.res.AndrolibResources import brut.androlib.res.AndrolibResources
import brut.androlib.res.data.ResPackage import brut.androlib.res.data.ResPackage
import brut.androlib.res.decoder.AXmlResourceParser import brut.androlib.res.decoder.AXmlResourceParser
import brut.androlib.res.decoder.ResAttrDecoder import brut.androlib.res.decoder.ResAttrDecoder
import brut.androlib.res.decoder.XmlPullStreamDecoder import brut.androlib.res.decoder.XmlPullStreamDecoder
import brut.androlib.res.xml.ResXmlPatcher
import brut.directory.ExtFile import brut.directory.ExtFile
import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO import lanchon.multidexlib2.DexIO
@ -40,32 +43,36 @@ val NAMER = BasicDexFileNamer()
class Patcher( class Patcher(
private val options: PatcherOptions private val options: PatcherOptions
) { ) {
val packageVersion: String val data: PatcherData
val packageName: String
private lateinit var usesFramework: UsesFramework
private val patcherData: PatcherData
private val opcodes: Opcodes private val opcodes: Opcodes
init { init {
val extFileInput = ExtFile(options.inputFile) val extInputFile = ExtFile(options.inputFile)
val outDir = File(options.resourceCacheDirectory) val outDir = File(options.resourceCacheDirectory)
if (outDir.exists()) outDir.deleteRecursively() if (outDir.exists()) outDir.deleteRecursively()
outDir.mkdir() outDir.mkdirs()
// load the resource table from the input file
val androlib = Androlib() val androlib = Androlib()
val resourceTable = androlib.getResTable(extFileInput, true) val resourceTable = androlib.getResTable(extInputFile, true)
val packageMetadata = PackageMetadata()
if (options.patchResources) { if (options.patchResources) {
// 1. decode resources to cache directory // decode resources to cache directory
androlib.decodeManifestWithResources(extFileInput, outDir, resourceTable) androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
androlib.decodeResourcesFull(extFileInput, outDir, resourceTable) androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
// 2. read framework ids from the resource table // read additional metadata from the resource table
usesFramework = UsesFramework() packageMetadata.metaInfo.usesFramework = UsesFramework().let { usesFramework ->
usesFramework.ids = resourceTable.listFramePackages().map { it.id }.sorted() usesFramework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
usesFramework
}
packageMetadata.metaInfo.doNotCompress = buildList {
androlib.recordUncompressedFiles(extInputFile, this)
}
} else { } else {
// create decoder for the resource table // create decoder for the resource table
val decoder = ResAttrDecoder() val decoder = ResAttrDecoder()
@ -80,19 +87,27 @@ class Patcher(
XmlPullStreamDecoder( XmlPullStreamDecoder(
axmlParser, AndrolibResources().resXmlSerializer axmlParser, AndrolibResources().resXmlSerializer
).decodeManifest( ).decodeManifest(
extFileInput.directory.getFileInput("AndroidManifest.xml"), nullOutputStream extInputFile.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
) )
} }
// set package information packageMetadata.packageName = resourceTable.currentResPackage.name
packageVersion = resourceTable.versionInfo.versionName packageMetadata.packageVersion = resourceTable.versionInfo.versionName
packageName = resourceTable.currentResPackage.name packageMetadata.metaInfo.versionInfo = resourceTable.versionInfo
packageMetadata.metaInfo.sdkInfo = resourceTable.sdkInfo
// read dex files // read dex files
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null) val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null).let { dexFile ->
// get the opcodes
opcodes = dexFile.opcodes opcodes = dexFile.opcodes
// save to patcher data dexFile
patcherData = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory) }
// finally create patcher data
data = PatcherData(
dexFile.classes.toMutableList(), options.resourceCacheDirectory, packageMetadata
)
} }
/** /**
@ -102,23 +117,25 @@ class Patcher(
* @param throwOnDuplicates If this is set to true, the patcher will throw an exception if a duplicate class has been found. * @param throwOnDuplicates If this is set to true, the patcher will throw an exception if a duplicate class has been found.
*/ */
fun addFiles( fun addFiles(
files: Iterable<File>, allowedOverwrites: Iterable<String> = emptyList(), throwOnDuplicates: Boolean = false files: List<File>, allowedOverwrites: Iterable<String> = emptyList(), throwOnDuplicates: Boolean = false
) { ) {
for (file in files) { for (file in files) {
val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null) MultiDexIO.readDexFile(true, file, NAMER, null, null).let { dexFile ->
for (classDef in dexFile.classes) { for (classDef in dexFile.classes) {
val e = patcherData.bytecodeData.classes.internalClasses.findIndexed { it.type == classDef.type } val e =
data.bytecodeData.classes.internalClasses.findIndexed { internalClass -> internalClass.type == classDef.type }
if (e != null) { if (e != null) {
if (throwOnDuplicates) { if (throwOnDuplicates) {
throw Exception("Class ${classDef.type} has already been added to the patcher.") throw Exception("Class ${classDef.type} has already been added to the patcher.")
} }
val (_, idx) = e val (_, idx) = e
if (allowedOverwrites.contains(classDef.type)) { if (allowedOverwrites.contains(classDef.type)) {
patcherData.bytecodeData.classes.internalClasses[idx] = classDef data.bytecodeData.classes.internalClasses[idx] = classDef
} }
continue continue
} }
patcherData.bytecodeData.classes.internalClasses.add(classDef) data.bytecodeData.classes.internalClasses.add(classDef)
}
} }
} }
} }
@ -126,11 +143,58 @@ class Patcher(
/** /**
* Save the patched dex file. * Save the patched dex file.
*/ */
fun save(): Map<String, MemoryDataStore> { fun save(): PatcherResult {
val packageMetadata = data.packageMetadata
val metaInfo = packageMetadata.metaInfo
if (options.patchResources) {
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
val androlibResources = AndrolibResources().let { resources ->
resources.buildOptions = BuildOptions().let { options ->
// TODO: options.useAapt2 = true
// TODO: options.aaptPath = ""
options.isFramework = metaInfo.isFrameworkApk
options.resourcesAreCompressed = metaInfo.compressionType
options.doNotCompress = metaInfo.doNotCompress
options
}
resources.setSdkInfo(metaInfo.sdkInfo)
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
resources
}
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
cacheDirectory.resolve("aapt_temp_file").let { temporalFile ->
val resDirectory = cacheDirectory.resolve("res")
val includedFiles =
metaInfo.usesFramework.ids.map { id -> androlibResources.getFrameworkApk(id, metaInfo.usesFramework.tag) }
.toTypedArray()
androlibResources.aaptPackage(
temporalFile, manifestFile, resDirectory, null,
null, includedFiles
)
// write packaged resources to cache directory
// TODO: consider returning a list of the files instead of extracting them to the cache directory,
// less disk but more ram usage
ExtFile(temporalFile).directory.copyToDir(cacheDirectory.resolve("build/"))
}
}
val newDexFile = object : DexFile { val newDexFile = object : DexFile {
override fun getClasses(): Set<ClassDef> { override fun getClasses(): Set<ClassDef> {
patcherData.bytecodeData.classes.applyProxies() data.bytecodeData.classes.applyProxies()
return ListBackedSet(patcherData.bytecodeData.classes.internalClasses) return ListBackedSet(data.bytecodeData.classes.internalClasses)
} }
override fun getOpcodes(): Opcodes { override fun getOpcodes(): Opcodes {
@ -138,21 +202,19 @@ class Patcher(
} }
} }
// build modified resources
if (options.patchResources) {
val extDir = ExtFile(options.resourceCacheDirectory)
// TODO: figure out why a new instance of Androlib is necessary here
Androlib().buildResources(extDir, usesFramework)
}
// write dex modified files // write dex modified files
val output = mutableMapOf<String, MemoryDataStore>() val dexFiles = mutableMapOf<String, MemoryDataStore>()
MultiDexIO.writeDexFile( MultiDexIO.writeDexFile(
true, -1, // core count true, -1, // core count
output, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null dexFiles, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
)
return PatcherResult(
dexFiles.map {
app.revanced.patcher.util.dex.DexFile(it.key, it.value)
},
metaInfo.doNotCompress.toList()
) )
return output
} }
/** /**
@ -160,7 +222,7 @@ class Patcher(
* @param patches [Patch]es The patches to add. * @param patches [Patch]es The patches to add.
*/ */
fun addPatches(patches: Iterable<Class<out Patch<Data>>>) { fun addPatches(patches: Iterable<Class<out Patch<Data>>>) {
patcherData.patches.addAll(patches) data.patches.addAll(patches)
} }
/** /**
@ -197,12 +259,12 @@ class Patcher(
// TODO: find a solution for this // TODO: find a solution for this
val data = if (isResourcePatch) { val data = if (isResourcePatch) {
patcherData.resourceData data.resourceData
} else { } else {
MethodSignatureResolver( MethodSignatureResolver(
patcherData.bytecodeData.classes.internalClasses, (patchInstance as BytecodePatch).signatures data.bytecodeData.classes.internalClasses, (patchInstance as BytecodePatch).signatures
).resolve(patcherData) ).resolve(data)
patcherData.bytecodeData data.bytecodeData
} }
return try { return try {
@ -225,7 +287,7 @@ class Patcher(
val appliedPatches = mutableListOf<String>() val appliedPatches = mutableListOf<String>()
return buildMap { return buildMap {
for (patch in patcherData.patches) { for (patch in data.patches) {
val result = applyPatch(patch, appliedPatches) val result = applyPatch(patch, appliedPatches)
val name = patch.patchName val name = patch.patchName

View File

@ -3,6 +3,7 @@ package app.revanced.patcher
import java.io.File import java.io.File
/** /**
* Options for a patcher.
* @param inputFile The input file (usually an apk file). * @param inputFile The input file (usually an apk file).
* @param resourceCacheDirectory Directory to cache resources. * @param resourceCacheDirectory Directory to cache resources.
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded. * @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.

View File

@ -0,0 +1,13 @@
package app.revanced.patcher
import app.revanced.patcher.util.dex.DexFile
/**
* The result of a patcher.
* @param dexFiles The patched dex files.
* @param doNotCompress List of relative paths to files to exclude from compressing.
*/
data class PatcherResult(
val dexFiles: List<DexFile>,
val doNotCompress: List<String>? = null
)

View File

@ -0,0 +1,13 @@
package app.revanced.patcher.data
import brut.androlib.meta.MetaInfo
/**
* Metadata about a package.
*/
class PackageMetadata {
lateinit var packageName: String
lateinit var packageVersion: String
internal val metaInfo: MetaInfo = MetaInfo()
}

View File

@ -7,10 +7,12 @@ import app.revanced.patcher.patch.base.Patch
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
import java.io.File import java.io.File
internal data class PatcherData( data class PatcherData(
val internalClasses: MutableList<ClassDef>, internal val internalClasses: MutableList<ClassDef>,
val resourceCacheDirectory: String internal val resourceCacheDirectory: String,
val packageMetadata: PackageMetadata
) { ) {
internal val patches = mutableListOf<Class<out Patch<Data>>>() internal val patches = mutableListOf<Class<out Patch<Data>>>()
internal val bytecodeData = BytecodeData(internalClasses) internal val bytecodeData = BytecodeData(internalClasses)

View File

@ -41,13 +41,15 @@ object PatchExtensions {
val Class<out Patch<Data>>.patchName: String val Class<out Patch<Data>>.patchName: String
get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version
val Class<out Patch<Data>>.excludeByDefault get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Patch::class)!!.excludeByDefault
val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description
val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Dependencies::class)?.dependencies val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Dependencies::class)?.dependencies
val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages
} }
object MethodSignatureExtensions { object MethodSignatureExtensions {
val MethodSignature.name: String get() = javaClass.recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName val MethodSignature.name: String
get() = javaClass.recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
val MethodSignature.version get() = javaClass.recursiveAnnotation(Version::class)?.version ?: "0.0.1" val MethodSignature.version get() = javaClass.recursiveAnnotation(Version::class)?.version ?: "0.0.1"
val MethodSignature.description get() = javaClass.recursiveAnnotation(Description::class)?.description val MethodSignature.description get() = javaClass.recursiveAnnotation(Description::class)?.description
val MethodSignature.compatiblePackages get() = javaClass.recursiveAnnotation(Compatibility::class)?.compatiblePackages val MethodSignature.compatiblePackages get() = javaClass.recursiveAnnotation(Compatibility::class)?.compatiblePackages

View File

@ -10,7 +10,7 @@ import kotlin.reflect.KClass
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented @MustBeDocumented
annotation class Patch annotation class Patch(val excludeByDefault: Boolean = false)
/** /**
* Annotation for dependencies of [Patch]es . * Annotation for dependencies of [Patch]es .

View File

@ -0,0 +1,10 @@
package app.revanced.patcher.util.dex
import org.jf.dexlib2.writer.io.MemoryDataStore
/**
* Wrapper for dex files.
* @param name The original name of the dex file
* @param memoryDataStore The data store for the dex file.
*/
data class DexFile(val name: String, val memoryDataStore: MemoryDataStore)