mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 01:02:22 +01:00
arsclib
This commit is contained in:
parent
8749a61d39
commit
d48a8e697f
96 changed files with 2012 additions and 1292 deletions
42
arsclib-utils/.gitignore
vendored
Normal file
42
arsclib-utils/.gitignore
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
39
arsclib-utils/build.gradle.kts
Normal file
39
arsclib-utils/build.gradle.kts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("io.github.reandroid:ARSCLib:1.1.7")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
if (System.getenv("GITHUB_ACTOR") != null)
|
||||||
|
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 {
|
||||||
|
register<MavenPublication>("gpr") {
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package app.revanced.arsc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when working with [Apk]s.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
// TODO: this probably needs a better name but idk what to call it.
|
||||||
|
sealed class ApkException(message: String, throwable: Throwable? = null) : Exception(message, throwable) {
|
||||||
|
/**
|
||||||
|
* An exception when decoding resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class Decode(message: String, throwable: Throwable? = null) : ApkException(message, throwable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception when encoding resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class Encode(message: String, throwable: Throwable? = null) : ApkException(message, throwable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when a reference could not be resolved.
|
||||||
|
*
|
||||||
|
* @param ref The invalid reference.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class InvalidReference(ref: String, throwable: Throwable? = null) :
|
||||||
|
ApkException("Failed to resolve: $ref", throwable) {
|
||||||
|
constructor(type: String, name: String, throwable: Throwable? = null) : this("@$type/$name", throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when the [Apk] does not have a resource table, but was expected to have one.
|
||||||
|
*/
|
||||||
|
object MissingResourceTable : ApkException("Apk does not have a resource table.")
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package app.revanced.arsc.archive
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import app.revanced.arsc.logging.Logger
|
||||||
|
import app.revanced.arsc.resource.ResourceContainer
|
||||||
|
import app.revanced.arsc.resource.ResourceFile
|
||||||
|
import app.revanced.arsc.xml.LazyXMLInputSource
|
||||||
|
import com.reandroid.apk.ApkModule
|
||||||
|
import com.reandroid.archive.ByteInputSource
|
||||||
|
import com.reandroid.archive.InputSource
|
||||||
|
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock
|
||||||
|
import com.reandroid.arsc.chunk.xml.ResXmlDocument
|
||||||
|
import com.reandroid.xml.XMLDocument
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private fun isResXml(inputSource: InputSource) = inputSource.openStream().use { ResXmlDocument.isResXmlBlock(it) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for reading/writing files in an [ApkModule].
|
||||||
|
*
|
||||||
|
* @param module The [ApkModule] to operate on.
|
||||||
|
*/
|
||||||
|
class Archive(private val module: ApkModule) {
|
||||||
|
lateinit var resources: ResourceContainer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a [read] operation.
|
||||||
|
*
|
||||||
|
* @param xml Whether the contents were decoded from a [ResXmlDocument].
|
||||||
|
* @param data The contents of the file.
|
||||||
|
*/
|
||||||
|
class ReadResult(val xml: Boolean, val data: ByteArray)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The zip archive.
|
||||||
|
*/
|
||||||
|
private val archive = module.apkArchive
|
||||||
|
|
||||||
|
private val lockedFiles = mutableMapOf<String, ResourceFile>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the [ResourceFile], preventing it from being opened again until it is unlocked.
|
||||||
|
*/
|
||||||
|
fun lock(file: ResourceFile) {
|
||||||
|
val path = file.handle.archivePath
|
||||||
|
if (lockedFiles.contains(path)) {
|
||||||
|
throw ApkException.Decode("${file.handle.virtualPath} is locked. If you are a patch developer, make sure you always close files.")
|
||||||
|
}
|
||||||
|
lockedFiles[path] = file
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock the [ResourceFile], allowing patches to open it again.
|
||||||
|
*/
|
||||||
|
fun unlock(file: ResourceFile) {
|
||||||
|
lockedFiles.remove(file.handle.archivePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all open files and encodes all XML files to binary XML.
|
||||||
|
*
|
||||||
|
* @param logger The [Logger] of the [app.revanced.patcher.Patcher].
|
||||||
|
*/
|
||||||
|
fun cleanup(logger: Logger?) {
|
||||||
|
lockedFiles.values.toList().forEach {
|
||||||
|
logger?.warn("${it.handle.virtualPath} was never closed!")
|
||||||
|
it.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
archive.listInputSources().filterIsInstance<LazyXMLInputSource>()
|
||||||
|
.forEach(LazyXMLInputSource::encode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the archive to disk.
|
||||||
|
*
|
||||||
|
* @param output The file to write the updated archive to.
|
||||||
|
*/
|
||||||
|
fun save(output: File) = module.writeApk(output)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read an entry from the archive.
|
||||||
|
*
|
||||||
|
* @param path The archive path to read from.
|
||||||
|
* @return A [ReadResult] containing the contents of the entry.
|
||||||
|
*/
|
||||||
|
fun read(path: String) =
|
||||||
|
archive.getInputSource(path)?.let { inputSource ->
|
||||||
|
val xml = when {
|
||||||
|
inputSource is LazyXMLInputSource -> inputSource.document
|
||||||
|
isResXml(inputSource) -> module.loadResXmlDocument(
|
||||||
|
inputSource
|
||||||
|
).decodeToXml(resources.resourceTable.entryStore, resources.packageBlock?.id ?: 0)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadResult(
|
||||||
|
xml != null,
|
||||||
|
xml?.toText()?.toByteArray() ?: inputSource.openStream().use { it.readAllBytes() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the manifest from the archive as an [AndroidManifestBlock].
|
||||||
|
*
|
||||||
|
* @return The [AndroidManifestBlock] contained in this archive.
|
||||||
|
*/
|
||||||
|
fun readManifest(): AndroidManifestBlock =
|
||||||
|
archive.getInputSource(AndroidManifestBlock.FILE_NAME).openStream().use { AndroidManifestBlock.load(it) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads all dex files from the archive.
|
||||||
|
*
|
||||||
|
* @return A [Map] containing all the dex files.
|
||||||
|
*/
|
||||||
|
fun readDexFiles() = module.listDexFiles().associate { file -> file.name to file.openStream().use { it.readAllBytes() } }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the byte array to the archive entry.
|
||||||
|
*
|
||||||
|
* @param path The archive path to read from.
|
||||||
|
* @param content The content of the file.
|
||||||
|
*/
|
||||||
|
fun writeRaw(path: String, content: ByteArray) =
|
||||||
|
archive.add(ByteInputSource(content, path))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the XML to the entry associated.
|
||||||
|
*
|
||||||
|
* @param path The archive path to read from.
|
||||||
|
* @param document The XML document to encode.
|
||||||
|
*/
|
||||||
|
fun writeXml(path: String, document: XMLDocument) = archive.add(
|
||||||
|
LazyXMLInputSource(
|
||||||
|
path,
|
||||||
|
document,
|
||||||
|
resources,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package app.revanced.arsc.logging
|
||||||
|
interface Logger {
|
||||||
|
fun error(msg: String)
|
||||||
|
fun warn(msg: String)
|
||||||
|
fun info(msg: String)
|
||||||
|
fun trace(msg: String)
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import com.reandroid.arsc.coder.ValueDecoder
|
||||||
|
import com.reandroid.arsc.coder.EncodeResult
|
||||||
|
import com.reandroid.arsc.value.Entry
|
||||||
|
import com.reandroid.arsc.value.ValueType
|
||||||
|
import com.reandroid.arsc.value.array.ArrayBag
|
||||||
|
import com.reandroid.arsc.value.array.ArrayBagItem
|
||||||
|
import com.reandroid.arsc.value.plurals.PluralsBag
|
||||||
|
import com.reandroid.arsc.value.plurals.PluralsBagItem
|
||||||
|
import com.reandroid.arsc.value.plurals.PluralsQuantity
|
||||||
|
import com.reandroid.arsc.value.style.StyleBag
|
||||||
|
import com.reandroid.arsc.value.style.StyleBagItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A resource value.
|
||||||
|
*/
|
||||||
|
sealed class Resource {
|
||||||
|
internal abstract fun write(entry: Entry, resources: ResourceContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Resource.complex get() = when (this) {
|
||||||
|
is Scalar -> false
|
||||||
|
is Complex -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple resource.
|
||||||
|
*/
|
||||||
|
open class Scalar internal constructor(private val valueType: ValueType, private val value: Int) : Resource() {
|
||||||
|
protected open fun data(resources: ResourceContainer) = value
|
||||||
|
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
entry.setValueAsRaw(valueType, data(resources))
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.create(valueType, data(resources))
|
||||||
|
internal open fun toStyleItem(resources: ResourceContainer) = StyleBagItem.create(valueType, data(resources))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A marker class for complex resources.
|
||||||
|
*/
|
||||||
|
sealed class Complex : Resource()
|
||||||
|
|
||||||
|
private fun encoded(encodeResult: EncodeResult?) = encodeResult?.let { Scalar(it.valueType, it.value) }
|
||||||
|
?: throw ApkException.Encode("Failed to encode value")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a color.
|
||||||
|
*
|
||||||
|
* @param hex The hex value of the color.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun color(hex: String) = encoded(ValueDecoder.encodeColor(hex))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a dimension or fraction.
|
||||||
|
*
|
||||||
|
* @param value The dimension value such as 24dp.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun dimension(value: String) = encoded(ValueDecoder.encodeDimensionOrFraction(value))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a boolean resource.
|
||||||
|
*
|
||||||
|
* @param value The boolean.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun boolean(value: Boolean) = Scalar(ValueType.INT_BOOLEAN, if (value) -Int.MAX_VALUE else 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a float.
|
||||||
|
*
|
||||||
|
* @param n The number to encode.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun float(n: Float) = Scalar(ValueType.FLOAT, n.toBits())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an integer [Resource].
|
||||||
|
*
|
||||||
|
* @param n The number to encode.
|
||||||
|
* @return The integer [Resource].
|
||||||
|
*/
|
||||||
|
fun integer(n: Int) = Scalar(ValueType.INT_DEC, n)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a reference [Resource].
|
||||||
|
*
|
||||||
|
* @param resourceId The target resource.
|
||||||
|
* @return The reference resource.
|
||||||
|
*/
|
||||||
|
fun reference(resourceId: Int) = Scalar(ValueType.REFERENCE, resourceId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and create a reference [Resource].
|
||||||
|
*
|
||||||
|
* @see reference
|
||||||
|
* @param ref The reference string to resolve.
|
||||||
|
* @param resourceTable The resource table to resolve the reference with.
|
||||||
|
* @return The reference resource.
|
||||||
|
*/
|
||||||
|
fun reference(resourceTable: ResourceTable, ref: String) = reference(resourceTable.resolve(ref))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array [Resource].
|
||||||
|
*
|
||||||
|
* @param elements The elements of the array.
|
||||||
|
*/
|
||||||
|
class Array(private val elements: Collection<Scalar>) : Complex() {
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
ArrayBag.create(entry).addAll(elements.map { it.toArrayItem(resources) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A style resource.
|
||||||
|
*
|
||||||
|
* @param elements The attributes to override.
|
||||||
|
* @param parent A reference to the parent style.
|
||||||
|
*/
|
||||||
|
class Style(private val elements: Map<String, Scalar>, private val parent: String? = null) : Complex() {
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
val resTable = resources.resourceTable
|
||||||
|
val style = StyleBag.create(entry)
|
||||||
|
parent?.let {
|
||||||
|
style.parentId = resTable.resolve(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
style.putAll(
|
||||||
|
elements.asIterable().associate {
|
||||||
|
StyleBag.resolve(resTable.encodeMaterials, it.key) to it.value.toStyleItem(resources)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A quantity string [Resource].
|
||||||
|
*
|
||||||
|
* @param elements A map of the quantity to the corresponding string.
|
||||||
|
*/
|
||||||
|
class Plurals(private val elements: Map<String, String>) : Complex() {
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
val plurals = PluralsBag.create(entry)
|
||||||
|
|
||||||
|
plurals.putAll(elements.asIterable().associate { (k, v) ->
|
||||||
|
PluralsQuantity.value(k) to PluralsBagItem.string(resources.getOrCreateTableString(v))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string [Resource].
|
||||||
|
*
|
||||||
|
* @param value The string value.
|
||||||
|
*/
|
||||||
|
class StringResource(val value: String) : Scalar(ValueType.STRING, 0) {
|
||||||
|
private fun tableString(resources: ResourceContainer) = resources.getOrCreateTableString(value)
|
||||||
|
|
||||||
|
override fun data(resources: ResourceContainer) = tableString(resources).index
|
||||||
|
override fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.string(tableString(resources))
|
||||||
|
override fun toStyleItem(resources: ResourceContainer) = StyleBagItem.string(tableString(resources))
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import app.revanced.arsc.archive.Archive
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeUtil
|
||||||
|
import com.reandroid.arsc.chunk.TableBlock
|
||||||
|
import com.reandroid.arsc.value.Entry
|
||||||
|
import com.reandroid.arsc.value.ResConfig
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A high-level API for modifying the resources contained in an Apk.
|
||||||
|
*
|
||||||
|
* @param tableBlock The resources.arsc file of this Apk.
|
||||||
|
*/
|
||||||
|
class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock?) {
|
||||||
|
internal val packageBlock = tableBlock?.pickOne() // Pick the main PackageBlock.
|
||||||
|
|
||||||
|
internal lateinit var resourceTable: ResourceTable
|
||||||
|
|
||||||
|
init {
|
||||||
|
archive.resources = this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectPackageBlock() = packageBlock ?: throw ApkException.MissingResourceTable
|
||||||
|
|
||||||
|
internal fun getOrCreateTableString(value: String) =
|
||||||
|
tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkException.MissingResourceTable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of the [Entry] to the one specified.
|
||||||
|
*
|
||||||
|
* @param value The new value.
|
||||||
|
*/
|
||||||
|
private fun Entry.setTo(value: Resource) {
|
||||||
|
val savedRef = specReference
|
||||||
|
ensureComplex(value.complex)
|
||||||
|
if (savedRef != 0) {
|
||||||
|
// Preserve the entry name by restoring the previous spec block reference (if present).
|
||||||
|
specReference = savedRef
|
||||||
|
}
|
||||||
|
|
||||||
|
value.write(this, this@ResourceContainer)
|
||||||
|
resourceTable.registerChanged(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an [Entry] from the resource table.
|
||||||
|
*
|
||||||
|
* @param type The resource type.
|
||||||
|
* @param name The resource name.
|
||||||
|
* @param qualifiers The variant to use.
|
||||||
|
*/
|
||||||
|
private fun getEntry(type: String, name: String, qualifiers: String?): Entry? {
|
||||||
|
val resourceId = try {
|
||||||
|
resourceTable.resolve("@$type/$name")
|
||||||
|
} catch (_: ApkException.InvalidReference) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = ResConfig.parse(qualifiers)
|
||||||
|
return tableBlock?.resolveReference(resourceId)?.singleOrNull { it.resConfig == config }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [ResourceFile.Handle] that can be used to open a [ResourceFile].
|
||||||
|
* This may involve looking it up in the resource table to find the actual location in the archive.
|
||||||
|
*
|
||||||
|
* @param resPath The path of the resource.
|
||||||
|
*/
|
||||||
|
private fun createHandle(resPath: String): ResourceFile.Handle {
|
||||||
|
if (resPath.startsWith("res/values")) throw ApkException.Decode("Decoding the resource table as a file is not supported")
|
||||||
|
|
||||||
|
var callback = {}
|
||||||
|
var archivePath = resPath
|
||||||
|
|
||||||
|
if (tableBlock != null && resPath.startsWith("res/") && resPath.count { it == '/' } == 2) {
|
||||||
|
val file = File(resPath)
|
||||||
|
|
||||||
|
val qualifiers = EncodeUtil.getQualifiersFromResFile(file)
|
||||||
|
val type = EncodeUtil.getTypeNameFromResFile(file)
|
||||||
|
val name = file.nameWithoutExtension
|
||||||
|
|
||||||
|
// The resource file names that the app developers used may have been minified, so we have to resolve it with the resource table.
|
||||||
|
// Example: res/drawable-hdpi/icon.png -> res/4a.png
|
||||||
|
val resolvedPath = getEntry(type, name, qualifiers)?.resValue?.valueAsString
|
||||||
|
|
||||||
|
if (resolvedPath != null) {
|
||||||
|
archivePath = resolvedPath
|
||||||
|
} else {
|
||||||
|
// An entry for this specific resource file was not found in the resource table, so we have to register it after we save.
|
||||||
|
callback = { set(type, name, StringResource(archivePath), qualifiers) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResourceFile.Handle(resPath, archivePath, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update an Android resource.
|
||||||
|
*
|
||||||
|
* @param type The resource type.
|
||||||
|
* @param name The name of the resource.
|
||||||
|
* @param value The resource data.
|
||||||
|
* @param configuration The resource configuration.
|
||||||
|
*/
|
||||||
|
fun set(type: String, name: String, value: Resource, configuration: String? = null) =
|
||||||
|
expectPackageBlock().getOrCreate(configuration, type, name).also { it.setTo(value) }.resourceId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update multiple resources in an ARSC type block.
|
||||||
|
*
|
||||||
|
* @param type The resource type.
|
||||||
|
* @param map A map of resource names to the corresponding value.
|
||||||
|
* @param configuration The resource configuration.
|
||||||
|
*/
|
||||||
|
fun setGroup(type: String, map: Map<String, Resource>, configuration: String? = null) {
|
||||||
|
expectPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply {
|
||||||
|
map.forEach { (name, value) -> getOrCreateEntry(name).setTo(value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a resource file, creating it if the file does not exist.
|
||||||
|
*
|
||||||
|
* @param path The resource file path.
|
||||||
|
* @return The corresponding [ResourceFile],
|
||||||
|
*/
|
||||||
|
fun openFile(path: String) = ResourceFile(
|
||||||
|
createHandle(path), archive
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the [PackageBlock] name to match the manifest.
|
||||||
|
*/
|
||||||
|
fun refreshPackageName() {
|
||||||
|
packageBlock?.name = archive.readManifest().packageName
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import app.revanced.arsc.archive.Archive
|
||||||
|
import com.reandroid.xml.XMLDocument
|
||||||
|
import com.reandroid.xml.XMLException
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A resource file inside an [Apk].
|
||||||
|
*/
|
||||||
|
class ResourceFile private constructor(
|
||||||
|
internal val handle: Handle,
|
||||||
|
private val archive: Archive,
|
||||||
|
readResult: Archive.ReadResult?
|
||||||
|
) :
|
||||||
|
Closeable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param virtualPath The resource file path (res/drawable-hdpi/icon.png)
|
||||||
|
* @param archivePath The actual file path in the archive (res/4a.png)
|
||||||
|
* @param close An action to perform when the file associated with this handle is closed
|
||||||
|
*/
|
||||||
|
internal data class Handle(val virtualPath: String, val archivePath: String, val close: () -> Unit)
|
||||||
|
|
||||||
|
private var changed = false
|
||||||
|
private val xml = readResult?.xml ?: handle.virtualPath.endsWith(".xml")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param handle The [Handle] associated with this file
|
||||||
|
* @param archive The [Archive] that the file resides in
|
||||||
|
*/
|
||||||
|
internal constructor(handle: Handle, archive: Archive) : this(
|
||||||
|
handle,
|
||||||
|
archive,
|
||||||
|
try {
|
||||||
|
archive.read(handle.archivePath)
|
||||||
|
} catch (e: XMLException) {
|
||||||
|
throw ApkException.Decode("Failed to decode XML while reading ${handle.virtualPath}", e)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw ApkException.Decode("Could not read ${handle.virtualPath}", e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var contents = readResult?.data ?: ByteArray(0)
|
||||||
|
set(value) {
|
||||||
|
changed = true
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
val exists = readResult != null
|
||||||
|
|
||||||
|
override fun toString() = handle.virtualPath
|
||||||
|
|
||||||
|
init {
|
||||||
|
archive.lock(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
if (changed) {
|
||||||
|
val path = handle.archivePath
|
||||||
|
if (xml) archive.writeXml(
|
||||||
|
path,
|
||||||
|
try {
|
||||||
|
XMLDocument.load(String(contents))
|
||||||
|
} catch (e: XMLException) {
|
||||||
|
throw ApkException.Encode("Failed to parse XML while writing ${handle.virtualPath}", e)
|
||||||
|
}
|
||||||
|
) else archive.writeRaw(path, contents)
|
||||||
|
}
|
||||||
|
handle.close()
|
||||||
|
archive.unlock(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_BUFFER_SIZE = 4096
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inputStream(): InputStream = ByteArrayInputStream(contents)
|
||||||
|
fun outputStream(bufferSize: Int = DEFAULT_BUFFER_SIZE): OutputStream =
|
||||||
|
object : ByteArrayOutputStream(bufferSize) {
|
||||||
|
override fun close() {
|
||||||
|
this@ResourceFile.contents = if (buf.size > count) buf.copyOf(count) else buf
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeException
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeMaterials
|
||||||
|
import com.reandroid.arsc.util.FrameworkTable
|
||||||
|
import com.reandroid.arsc.value.Entry
|
||||||
|
import com.reandroid.common.TableEntryStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A high-level API for resolving resources in the resource table, which spans the entire [ApkBundle].
|
||||||
|
*/
|
||||||
|
class ResourceTable(base: ResourceContainer, all: Sequence<ResourceContainer>) {
|
||||||
|
private val packageName = base.packageBlock!!.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TableEntryStore] used to decode XML.
|
||||||
|
*/
|
||||||
|
internal val entryStore = TableEntryStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [EncodeMaterials] to use for resolving resources and encoding XML.
|
||||||
|
*/
|
||||||
|
internal val encodeMaterials: EncodeMaterials = object : EncodeMaterials() {
|
||||||
|
/*
|
||||||
|
Our implementation is more efficient because it does not have to loop through every single entry group
|
||||||
|
when the resource id cannot be found in the TableIdentifier, which does not update when you create a new resource.
|
||||||
|
It also looks at the entire table instead of just the current package.
|
||||||
|
*/
|
||||||
|
override fun resolveLocalResourceId(type: String, name: String) = resolveLocal(type, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource mappings which are generated when the [ApkBundle] is created.
|
||||||
|
*/
|
||||||
|
private val tableIdentifier = encodeMaterials.tableIdentifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A table of all the resources that have been changed or added.
|
||||||
|
*/
|
||||||
|
private val modifiedResources = HashMap<String, HashMap<String, Int>>()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a resource id for the specified resource.
|
||||||
|
* Cannot resolve resources from the android framework.
|
||||||
|
*
|
||||||
|
* @param type The type of the resource.
|
||||||
|
* @param name The name of the resource.
|
||||||
|
* @return The id of the resource.
|
||||||
|
*/
|
||||||
|
fun resolveLocal(type: String, name: String) =
|
||||||
|
modifiedResources[type]?.get(name)
|
||||||
|
?: tableIdentifier.get(packageName, type, name)?.resourceId
|
||||||
|
?: throw ApkException.InvalidReference(
|
||||||
|
type,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a resource id for the specified resource.
|
||||||
|
*
|
||||||
|
* @param reference The resource reference string.
|
||||||
|
* @return The id of the resource.
|
||||||
|
*/
|
||||||
|
fun resolve(reference: String) = try {
|
||||||
|
encodeMaterials.resolveReference(reference)
|
||||||
|
} catch (e: EncodeException) {
|
||||||
|
throw ApkException.InvalidReference(reference, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the [ResourceTable] that an [Entry] has been created or modified.
|
||||||
|
*/
|
||||||
|
internal fun registerChanged(entry: Entry) {
|
||||||
|
modifiedResources.getOrPut(entry.typeName, ::HashMap)[entry.name] = entry.resourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
all.forEach {
|
||||||
|
it.tableBlock?.let { table ->
|
||||||
|
entryStore.add(table)
|
||||||
|
tableIdentifier.load(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.resourceTable = this
|
||||||
|
}
|
||||||
|
|
||||||
|
base.also {
|
||||||
|
encodeMaterials.currentPackage = it.packageBlock
|
||||||
|
|
||||||
|
it.tableBlock!!.frameWorks.forEach { fw ->
|
||||||
|
if (fw is FrameworkTable) {
|
||||||
|
entryStore.add(fw)
|
||||||
|
encodeMaterials.addFramework(fw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package app.revanced.arsc.xml
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import app.revanced.arsc.resource.ResourceContainer
|
||||||
|
import app.revanced.arsc.resource.boolean
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeException
|
||||||
|
import com.reandroid.apk.xmlencoder.XMLEncodeSource
|
||||||
|
import com.reandroid.arsc.chunk.xml.ResXmlDocument
|
||||||
|
import com.reandroid.xml.XMLDocument
|
||||||
|
import com.reandroid.xml.XMLElement
|
||||||
|
import com.reandroid.xml.source.XMLDocumentSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive input source that lazily encodes the [XMLDocument] when you read from it.
|
||||||
|
*
|
||||||
|
* @param name The file name of this input source.
|
||||||
|
* @param document The [XMLDocument] to encode.
|
||||||
|
* @param resources The [ResourceContainer] to use for encoding.
|
||||||
|
*/
|
||||||
|
internal class LazyXMLInputSource(
|
||||||
|
name: String,
|
||||||
|
val document: XMLDocument,
|
||||||
|
private val resources: ResourceContainer
|
||||||
|
) : XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document)) {
|
||||||
|
private var ready = false
|
||||||
|
|
||||||
|
private fun XMLElement.registerIds() {
|
||||||
|
listAttributes().forEach { attr ->
|
||||||
|
if (attr.value.startsWith("@+id/")) {
|
||||||
|
val name = attr.value.split('/').last()
|
||||||
|
resources.set("id", name, boolean(false))
|
||||||
|
attr.value = "@id/$name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listChildElements().forEach { it.registerIds() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getResXmlBlock(): ResXmlDocument {
|
||||||
|
if (!ready) {
|
||||||
|
throw ApkException.Encode("$name has not been encoded yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getResXmlBlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the [XMLDocument] associated with this input source.
|
||||||
|
*/
|
||||||
|
fun encode() {
|
||||||
|
// Handle all @+id/id_name references in the document.
|
||||||
|
document.documentElement.registerIds()
|
||||||
|
|
||||||
|
ready = true
|
||||||
|
|
||||||
|
// This will call XMLEncodeSource.getResXmlBlock(), which will encode the document if it has not already been encoded.
|
||||||
|
try {
|
||||||
|
resXmlBlock
|
||||||
|
} catch (e: EncodeException) {
|
||||||
|
throw EncodeException("Failed to encode $name", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,71 +1,3 @@
|
||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.8.20"
|
kotlin("jvm") version "1.8.20" apply false
|
||||||
`maven-publish`
|
|
||||||
}
|
|
||||||
|
|
||||||
group = "app.revanced"
|
|
||||||
|
|
||||||
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
|
|
||||||
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
|
||||||
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
|
||||||
credentials {
|
|
||||||
username = githubUsername
|
|
||||||
password = githubPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("xpp3:xpp3:1.1.4c")
|
|
||||||
implementation("app.revanced:smali:2.5.3-a3836654")
|
|
||||||
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
|
|
||||||
implementation("app.revanced:apktool-lib:2.7.0")
|
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
testLogging {
|
|
||||||
events("PASSED", "SKIPPED", "FAILED")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processResources {
|
|
||||||
expand("projectVersion" to project.version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
withSourcesJar()
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(11)
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
|
||||||
repositories {
|
|
||||||
if (System.getenv("GITHUB_ACTOR") != null)
|
|
||||||
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 {
|
|
||||||
register<MavenPublication>("gpr") {
|
|
||||||
from(components["java"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
61
revanced-patcher/build.gradle.kts
Normal file
61
revanced-patcher/build.gradle.kts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("xpp3:xpp3:1.1.4c")
|
||||||
|
implementation("app.revanced:smali:2.5.3-a3836654")
|
||||||
|
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
|
||||||
|
implementation("io.github.reandroid:ARSCLib:1.1.7")
|
||||||
|
implementation(project(":arsclib-utils"))
|
||||||
|
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
||||||
|
|
||||||
|
compileOnly("com.google.android:android:4.1.1.4")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processResources {
|
||||||
|
expand("projectVersion" to project.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
if (System.getenv("GITHUB_ACTOR") != null)
|
||||||
|
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 {
|
||||||
|
register<MavenPublication>("gpr") {
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
revanced-patcher/settings.gradle.kts
Normal file
1
revanced-patcher/settings.gradle.kts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = "revanced-patcher"
|
113
revanced-patcher/src/main/kotlin/app/revanced/patcher/Context.kt
Normal file
113
revanced-patcher/src/main/kotlin/app/revanced/patcher/Context.kt
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.arsc.resource.ResourceContainer
|
||||||
|
import app.revanced.patcher.apk.Apk
|
||||||
|
import app.revanced.patcher.apk.ApkBundle
|
||||||
|
import app.revanced.arsc.resource.ResourceFile
|
||||||
|
import app.revanced.patcher.util.method.MethodWalker
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.StringWriter
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
import javax.xml.transform.TransformerFactory
|
||||||
|
import javax.xml.transform.dom.DOMSource
|
||||||
|
import javax.xml.transform.stream.StreamResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common class to constrain [Context] to [BytecodeContext] and [ResourceContext].
|
||||||
|
* @param apkBundle The [ApkBundle] for this context.
|
||||||
|
*/
|
||||||
|
sealed class Context(val apkBundle: ApkBundle)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context for the bytecode of an [Apk.Base] file.
|
||||||
|
*
|
||||||
|
* @param apkBundle The [ApkBundle] for this context.
|
||||||
|
*/
|
||||||
|
class BytecodeContext internal constructor(apkBundle: ApkBundle) : Context(apkBundle) {
|
||||||
|
/**
|
||||||
|
* The list of classes.
|
||||||
|
*/
|
||||||
|
val classes = apkBundle.base.bytecodeData.classes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||||
|
*
|
||||||
|
* @param startMethod The method to start at.
|
||||||
|
* @return A [MethodWalker] instance.
|
||||||
|
*/
|
||||||
|
fun traceMethodCalls(startMethod: Method) = MethodWalker(this, startMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A context for [Apk] file resources.
|
||||||
|
*
|
||||||
|
* @param apkBundle the [ApkBundle] for this context.
|
||||||
|
*/
|
||||||
|
class ResourceContext internal constructor(apkBundle: ApkBundle) : Context(apkBundle) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open an [DomFileEditor] for a given DOM file.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream to read the DOM file from.
|
||||||
|
* @return A [DomFileEditor] instance.
|
||||||
|
*/
|
||||||
|
fun openXmlFile(inputStream: InputStream) = DomFileEditor(inputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a [DomFileEditor] for a resource file in the archive.
|
||||||
|
*
|
||||||
|
* @see [ResourceContainer.openFile]
|
||||||
|
* @param path The resource file path.
|
||||||
|
* @return A [DomFileEditor].
|
||||||
|
*/
|
||||||
|
fun ResourceContainer.openXmlFile(path: String) = DomFileEditor(openFile(path))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for a file that can be edited as a dom document.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream to read the xml file from.
|
||||||
|
* @param onSave A callback that will be called when the editor is closed to save the file.
|
||||||
|
*/
|
||||||
|
class DomFileEditor internal constructor(
|
||||||
|
private val inputStream: InputStream,
|
||||||
|
private val onSave: ((String) -> Unit)? = null
|
||||||
|
) : Closeable {
|
||||||
|
private var closed: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The document of the xml file.
|
||||||
|
*/
|
||||||
|
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
||||||
|
.also(Document::normalize)
|
||||||
|
|
||||||
|
internal constructor(file: ResourceFile) : this(
|
||||||
|
file.inputStream(),
|
||||||
|
{
|
||||||
|
file.contents = it.toByteArray()
|
||||||
|
file.close()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the editor and writes back to the file.
|
||||||
|
*/
|
||||||
|
override fun close() {
|
||||||
|
if (closed) return
|
||||||
|
|
||||||
|
inputStream.close()
|
||||||
|
|
||||||
|
onSave?.let { callback ->
|
||||||
|
// Save the updated file.
|
||||||
|
val writer = StringWriter()
|
||||||
|
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), StreamResult(writer))
|
||||||
|
callback(writer.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
}
|
220
revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
220
revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.apk.Apk
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
|
||||||
|
import app.revanced.patcher.patch.*
|
||||||
|
import app.revanced.patcher.util.VersionReader
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import lanchon.multidexlib2.BasicDexFileNamer
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.util.function.Function
|
||||||
|
|
||||||
|
typealias ExecutedPatchResults = Flow<Pair<String, PatchException?>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ReVanced Patcher.
|
||||||
|
* @param options The options for the patcher.
|
||||||
|
* @param patches The patches to use.
|
||||||
|
* @param integrations The integrations to merge if necessary. Must be dex files or dex file container such as ZIP, APK or DEX files.
|
||||||
|
*/
|
||||||
|
class Patcher(private val options: PatcherOptions, patches: Iterable<PatchClass>, integrations: Iterable<File>) :
|
||||||
|
Function<Boolean, ExecutedPatchResults> {
|
||||||
|
private val context = PatcherContext(options, patches.toList(), integrations)
|
||||||
|
private val logger = options.logger
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* The version of the ReVanced Patcher.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
val version = VersionReader.read()
|
||||||
|
|
||||||
|
@Suppress("SpellCheckingInspection")
|
||||||
|
internal val dexFileNamer = BasicDexFileNamer()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
/**
|
||||||
|
* Returns true if at least one patches or its dependencies matches the given predicate.
|
||||||
|
*/
|
||||||
|
fun PatchClass.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean =
|
||||||
|
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
|
||||||
|
|
||||||
|
// Determine if merging integrations is required.
|
||||||
|
for (patch in context.patches) {
|
||||||
|
if (patch.anyRecursively { it.requiresIntegrations }) {
|
||||||
|
context.integrations.merge = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the patcher.
|
||||||
|
*
|
||||||
|
* @param stopOnError If true, the patches will stop on the first error.
|
||||||
|
* @return A pair of the name of the [Patch] and a [PatchException] if it failed.
|
||||||
|
*/
|
||||||
|
override fun apply(stopOnError: Boolean) = flow {
|
||||||
|
/**
|
||||||
|
* Execute a [Patch] and its dependencies recursively.
|
||||||
|
*
|
||||||
|
* @param patchClass The [Patch] to execute.
|
||||||
|
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
|
||||||
|
*/
|
||||||
|
suspend fun executePatch(
|
||||||
|
patchClass: PatchClass,
|
||||||
|
executedPatches: HashMap<String, ExecutedPatch>
|
||||||
|
) {
|
||||||
|
val patchName = patchClass.patchName
|
||||||
|
|
||||||
|
// If the patch has already executed silently skip it.
|
||||||
|
if (executedPatches.contains(patchName)) {
|
||||||
|
if (!executedPatches[patchName]!!.success)
|
||||||
|
throw PatchException("'$patchName' did not succeed previously")
|
||||||
|
|
||||||
|
logger.trace("Skipping '$patchName' because it has already been executed")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively execute all dependency patches.
|
||||||
|
patchClass.dependencies?.forEach { dependencyClass ->
|
||||||
|
val dependency = dependencyClass.java
|
||||||
|
|
||||||
|
try {
|
||||||
|
executePatch(dependency, executedPatches)
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
throw PatchException(
|
||||||
|
"'$patchName' depends on '${dependency.patchName}' " +
|
||||||
|
"but the following exception was raised: ${throwable.cause?.stackTraceToString() ?: throwable.message}",
|
||||||
|
throwable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
|
||||||
|
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
|
||||||
|
|
||||||
|
// TODO: implement this in a more polymorphic way.
|
||||||
|
val patchContext = if (isResourcePatch) {
|
||||||
|
context.resourceContext
|
||||||
|
} else {
|
||||||
|
context.bytecodeContext.apply {
|
||||||
|
val bytecodePatch = patchInstance as BytecodePatch
|
||||||
|
bytecodePatch.fingerprints?.resolveUsingLookupMap(context.bytecodeContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
|
||||||
|
|
||||||
|
var success = false
|
||||||
|
try {
|
||||||
|
patchInstance.execute(patchContext)
|
||||||
|
|
||||||
|
success = true
|
||||||
|
} catch (patchException: PatchException) {
|
||||||
|
throw patchException
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
throw PatchException("Unhandled patch exception: ${throwable.message}", throwable)
|
||||||
|
} finally {
|
||||||
|
executedPatches[patchName] = ExecutedPatch(patchInstance, success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.integrations.merge) context.integrations.merge(logger)
|
||||||
|
|
||||||
|
logger.trace("Initialize lookup maps for method MethodFingerprint resolution")
|
||||||
|
|
||||||
|
MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext)
|
||||||
|
|
||||||
|
logger.info("Executing patches")
|
||||||
|
|
||||||
|
// Key is patch name.
|
||||||
|
LinkedHashMap<String, ExecutedPatch>().apply {
|
||||||
|
context.patches.forEach { patch ->
|
||||||
|
var exception: PatchException? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
executePatch(patch, this)
|
||||||
|
} catch (patchException: PatchException) {
|
||||||
|
exception = patchException
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(patch.patchName to exception)
|
||||||
|
|
||||||
|
if (stopOnError && exception != null) return@flow
|
||||||
|
}
|
||||||
|
}.let {
|
||||||
|
it.values
|
||||||
|
.filter(ExecutedPatch::success)
|
||||||
|
.map(ExecutedPatch::patchInstance)
|
||||||
|
.filterIsInstance(Closeable::class.java)
|
||||||
|
.asReversed().forEach { patch ->
|
||||||
|
try {
|
||||||
|
patch.close()
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
val patchException =
|
||||||
|
if (throwable is PatchException) throwable
|
||||||
|
else PatchException(throwable)
|
||||||
|
|
||||||
|
val patchName = (patch as Patch<Context>).javaClass.patchName
|
||||||
|
|
||||||
|
logger.error("Failed to close '$patchName': ${patchException.stackTraceToString()}")
|
||||||
|
|
||||||
|
emit(patchName to patchException)
|
||||||
|
|
||||||
|
// This is not failsafe. If a patch throws an exception while closing,
|
||||||
|
// the other patches that depend on it may fail.
|
||||||
|
if (stopOnError) return@flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodFingerprint.clearFingerprintResolutionLookupMaps()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish patching all [Apk]s.
|
||||||
|
*
|
||||||
|
* @return The [PatcherResult] of the [Patcher].
|
||||||
|
*/
|
||||||
|
fun finish(): PatcherResult {
|
||||||
|
val patchResults = buildList {
|
||||||
|
logger.info("Processing patched apks")
|
||||||
|
options.apkBundle.cleanup(options).forEach { result ->
|
||||||
|
if (result.exception != null) {
|
||||||
|
logger.error("Got exception while processing ${result.apk}: ${result.exception.stackTraceToString()}")
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val patch = result.let {
|
||||||
|
when (it.apk) {
|
||||||
|
is Apk.Base -> PatcherResult.Patch.Base(it.apk)
|
||||||
|
is Apk.Split -> PatcherResult.Patch.Split(it.apk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(patch)
|
||||||
|
|
||||||
|
logger.info("Patched ${result.apk}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PatcherResult(patchResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A result of executing a [Patch].
|
||||||
|
*
|
||||||
|
* @param patchInstance The instance of the [Patch] that was executed.
|
||||||
|
* @param success The result of the [Patch].
|
||||||
|
*/
|
||||||
|
internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)
|
|
@ -0,0 +1,55 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.logging.Logger
|
||||||
|
import app.revanced.patcher.patch.PatchClass
|
||||||
|
import app.revanced.patcher.util.ClassMerger.merge
|
||||||
|
import lanchon.multidexlib2.MultiDexIO
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class PatcherContext(
|
||||||
|
options: PatcherOptions,
|
||||||
|
internal val patches: List<PatchClass>,
|
||||||
|
integrations: Iterable<File>
|
||||||
|
) {
|
||||||
|
internal val integrations = Integrations(this, integrations)
|
||||||
|
internal val bytecodeContext = BytecodeContext(options.apkBundle)
|
||||||
|
internal val resourceContext = ResourceContext(options.apkBundle)
|
||||||
|
|
||||||
|
internal class Integrations(val context: PatcherContext, private val dexContainers: Iterable<File>) {
|
||||||
|
var merge = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge integrations.
|
||||||
|
* @param logger A logger.
|
||||||
|
*/
|
||||||
|
fun merge(logger: Logger) {
|
||||||
|
context.bytecodeContext.classes.apply {
|
||||||
|
for (integrations in dexContainers) {
|
||||||
|
logger.info("Merging $integrations")
|
||||||
|
|
||||||
|
for (classDef in MultiDexIO.readDexFile(true, integrations, Patcher.dexFileNamer, null, null).classes) {
|
||||||
|
val type = classDef.type
|
||||||
|
|
||||||
|
val existingClassIndex = this.indexOfFirst { it.type == type }
|
||||||
|
if (existingClassIndex == -1) {
|
||||||
|
logger.trace("Merging type $type")
|
||||||
|
add(classDef)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
logger.trace("Type $type exists. Adding missing methods and fields.")
|
||||||
|
|
||||||
|
get(existingClassIndex).apply {
|
||||||
|
merge(classDef, context.bytecodeContext, logger).let { mergedClass ->
|
||||||
|
if (mergedClass !== this) // referential equality check
|
||||||
|
set(existingClassIndex, mergedClass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.apk.ApkBundle
|
||||||
|
import app.revanced.patcher.logging.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for the [Patcher].
|
||||||
|
* @param apkBundle The [ApkBundle].
|
||||||
|
* @param logger Custom logger implementation for the [Patcher].
|
||||||
|
*/
|
||||||
|
class PatcherOptions(
|
||||||
|
internal val apkBundle: ApkBundle,
|
||||||
|
internal val logger: Logger = Logger.Nop
|
||||||
|
)
|
|
@ -0,0 +1,33 @@
|
||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.apk.Apk
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a patcher.
|
||||||
|
* @param apkFiles The patched [Apk] files.
|
||||||
|
*/
|
||||||
|
data class PatcherResult(val apkFiles: List<Patch>) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a patch.
|
||||||
|
*
|
||||||
|
* @param apk The patched [Apk] file.
|
||||||
|
*/
|
||||||
|
sealed class Patch(val apk: Apk) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a patch of an [Apk.Split] file.
|
||||||
|
*
|
||||||
|
* @param apk The patched [Apk.Split] file.
|
||||||
|
*/
|
||||||
|
class Split(apk: Apk.Split) : Patch(apk)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a patch of an [Apk.Split] file.
|
||||||
|
*
|
||||||
|
* @param apk The patched [Apk.Base] file.
|
||||||
|
*/
|
||||||
|
class Base(apk: Apk.Base) : Patch(apk)
|
||||||
|
}
|
||||||
|
}
|
270
revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/Apk.kt
Normal file
270
revanced-patcher/src/main/kotlin/app/revanced/patcher/apk/Apk.kt
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
|
package app.revanced.patcher.apk
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import app.revanced.arsc.archive.Archive
|
||||||
|
import app.revanced.arsc.resource.*
|
||||||
|
import app.revanced.patcher.Patcher
|
||||||
|
import app.revanced.patcher.PatcherOptions
|
||||||
|
import app.revanced.patcher.logging.asArscLogger
|
||||||
|
import app.revanced.patcher.util.ProxyBackedClassList
|
||||||
|
import com.reandroid.apk.ApkModule
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeException
|
||||||
|
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock
|
||||||
|
import com.reandroid.arsc.value.ResConfig
|
||||||
|
import lanchon.multidexlib2.*
|
||||||
|
import org.jf.dexlib2.dexbacked.DexBackedDexFile
|
||||||
|
import org.jf.dexlib2.iface.DexFile
|
||||||
|
import org.jf.dexlib2.iface.MultiDexContainer
|
||||||
|
import org.jf.dexlib2.writer.io.MemoryDataStore
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [Apk] file.
|
||||||
|
*/
|
||||||
|
sealed class Apk private constructor(module: ApkModule) {
|
||||||
|
/**
|
||||||
|
* A wrapper around the zip archive of this [Apk].
|
||||||
|
*
|
||||||
|
* @see Archive
|
||||||
|
*/
|
||||||
|
private val archive = Archive(module)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata of the [Apk].
|
||||||
|
*/
|
||||||
|
val packageMetadata = PackageMetadata(module.androidManifestBlock)
|
||||||
|
|
||||||
|
val resources = ResourceContainer(archive, module.tableBlock)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh updated resources and close any open files.
|
||||||
|
*
|
||||||
|
* @param options The [PatcherOptions] of the [Patcher].
|
||||||
|
*/
|
||||||
|
internal open fun cleanup(options: PatcherOptions) {
|
||||||
|
try {
|
||||||
|
archive.cleanup(options.logger.asArscLogger())
|
||||||
|
} catch (e: EncodeException) {
|
||||||
|
throw ApkException.Encode(e.message!!, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.refreshPackageName()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the [Apk] to a file.
|
||||||
|
*
|
||||||
|
* @param output The target file.
|
||||||
|
*/
|
||||||
|
fun write(output: File) = archive.save(output)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val manifest = "AndroidManifest.xml"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the [Module] and [Type] of an [ApkModule].
|
||||||
|
*
|
||||||
|
* @return A [Pair] containing the [Module] and [Type] of the [ApkModule].
|
||||||
|
*/
|
||||||
|
fun ApkModule.identify(): Pair<Module, Type> {
|
||||||
|
val manifestElement = androidManifestBlock.manifestElement
|
||||||
|
return when {
|
||||||
|
isBaseModule -> Module.Main to Type.Base
|
||||||
|
// The module is a base apk for a dynamic feature module if the "isFeatureModule" attribute is set to true.
|
||||||
|
manifestElement.searchAttributeByName("isFeatureModule")?.valueAsBoolean == true -> Module.DynamicFeature(
|
||||||
|
split
|
||||||
|
) to Type.Base
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val module = manifestElement.searchAttributeByName("configForSplit")?.let { Module.DynamicFeature(it.valueAsString) } ?: Module.Main
|
||||||
|
// Examples:
|
||||||
|
// config.xhdpi
|
||||||
|
// df_my_feature.config.en
|
||||||
|
val config = this.split.split(".").last()
|
||||||
|
|
||||||
|
val type = when {
|
||||||
|
// Language splits have a two-letter country code.
|
||||||
|
config.length == 2 -> Type.Language(config)
|
||||||
|
// Library splits use the target CPU architecture.
|
||||||
|
Split.Library.architectures.contains(config) -> Type.Library(config)
|
||||||
|
// Asset splits use the density.
|
||||||
|
ResConfig.Density.valueOf(config) != null -> Type.Asset(config)
|
||||||
|
else -> throw IllegalArgumentException("Invalid split config: $config")
|
||||||
|
}
|
||||||
|
|
||||||
|
module to type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inner class BytecodeData {
|
||||||
|
private val dexFile = MultiDexContainerBackedDexFile(object : MultiDexContainer<DexBackedDexFile> {
|
||||||
|
// Load all dex files from the apk module and create a dex entry for each of them.
|
||||||
|
private val entries = archive.readDexFiles()
|
||||||
|
.mapValues { (name, data) -> BasicDexEntry(this, name, RawDexIO.readRawDexFile(data, 0, null)) }
|
||||||
|
|
||||||
|
override fun getDexEntryNames() = entries.keys.toList()
|
||||||
|
override fun getEntry(entryName: String) = entries[entryName]
|
||||||
|
})
|
||||||
|
private val opcodes = dexFile.opcodes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The classes and proxied classes of the [Base] apk file.
|
||||||
|
*/
|
||||||
|
val classes = ProxyBackedClassList(dexFile.classes)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write [classes] to the archive.
|
||||||
|
*/
|
||||||
|
internal fun writeDexFiles() {
|
||||||
|
// Make sure to replace all classes with their proxy.
|
||||||
|
val classes = classes.also(ProxyBackedClassList::applyProxies)
|
||||||
|
val opcodes = opcodes
|
||||||
|
|
||||||
|
// Create patched dex files.
|
||||||
|
mutableMapOf<String, MemoryDataStore>().also {
|
||||||
|
val newDexFile = object : DexFile {
|
||||||
|
override fun getClasses() = classes.toSet()
|
||||||
|
override fun getOpcodes() = opcodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write modified dex files.
|
||||||
|
MultiDexIO.writeDexFile(
|
||||||
|
true, -1, // Core count.
|
||||||
|
it, Patcher.dexFileNamer, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
|
||||||
|
)
|
||||||
|
}.forEach { (name, store) ->
|
||||||
|
archive.writeRaw(name, store.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata about an [Apk] file.
|
||||||
|
*
|
||||||
|
* @param packageName The package name of the [Apk] file.
|
||||||
|
* @param packageVersion The package version of the [Apk] file.
|
||||||
|
*/
|
||||||
|
data class PackageMetadata(val packageName: String, val packageVersion: String?) {
|
||||||
|
internal constructor(manifestBlock: AndroidManifestBlock) : this(
|
||||||
|
manifestBlock.packageName ?: "unnamed split apk file", manifestBlock.versionName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [Apk] of type [Split].
|
||||||
|
*
|
||||||
|
* @param config The device configuration associated with this [Split], such as arm64_v8a, en or xhdpi.
|
||||||
|
* @see Apk
|
||||||
|
*/
|
||||||
|
sealed class Split(val config: String, module: ApkModule) : Apk(module) {
|
||||||
|
override fun toString() = "split_config.$config.apk"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The split apk file which contains libraries.
|
||||||
|
*
|
||||||
|
* @see Split
|
||||||
|
*/
|
||||||
|
class Library internal constructor(config: String, module: ApkModule) : Split(config, module) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* A set of all architectures supported by android.
|
||||||
|
*/
|
||||||
|
val architectures = setOf("armeabi_v7a", "arm64_v8a", "x86", "x86_64")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The split apk file which contains language strings.
|
||||||
|
*
|
||||||
|
* @see Split
|
||||||
|
*/
|
||||||
|
class Language internal constructor(config: String, module: ApkModule) : Split(config, module)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The split apk file which contains assets.
|
||||||
|
*
|
||||||
|
* @see Split
|
||||||
|
*/
|
||||||
|
class Asset internal constructor(config: String, module: ApkModule) : Split(config, module)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base apk file that is to be patched.
|
||||||
|
*
|
||||||
|
* @see Apk
|
||||||
|
*/
|
||||||
|
class Base internal constructor(module: ApkModule) : Apk(module) {
|
||||||
|
/**
|
||||||
|
* Data of the [Base] apk file.
|
||||||
|
*/
|
||||||
|
internal val bytecodeData = BytecodeData()
|
||||||
|
|
||||||
|
override fun toString() = "base.apk"
|
||||||
|
|
||||||
|
override fun cleanup(options: PatcherOptions) {
|
||||||
|
super.cleanup(options)
|
||||||
|
|
||||||
|
options.logger.info("Writing patched dex files")
|
||||||
|
bytecodeData.writeDexFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The module that the [ApkModule] belongs to.
|
||||||
|
*/
|
||||||
|
sealed class Module {
|
||||||
|
/**
|
||||||
|
* The default [Module] that is always installed by software repositories.
|
||||||
|
*/
|
||||||
|
object Main : Module()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Module] that can be installed later by software repositories when requested by the application.
|
||||||
|
*
|
||||||
|
* @param name The name of the feature.
|
||||||
|
*/
|
||||||
|
data class DynamicFeature(val name: String) : Module()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the [ApkModule].
|
||||||
|
*/
|
||||||
|
sealed class Type {
|
||||||
|
/**
|
||||||
|
* The main Apk of a [Module].
|
||||||
|
*/
|
||||||
|
object Base : Type()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A superclass for all split configuration types.
|
||||||
|
*
|
||||||
|
* @param target The target device configuration.
|
||||||
|
*/
|
||||||
|
sealed class SplitConfig(val target: String) : Type()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Type] of an apk containing native libraries.
|
||||||
|
*
|
||||||
|
* @param architecture The target CPU architecture.
|
||||||
|
*/
|
||||||
|
data class Library(val architecture: String) : SplitConfig(architecture)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Type] for an Apk containing language resources.
|
||||||
|
*
|
||||||
|
* @param language The target language code.
|
||||||
|
*/
|
||||||
|
data class Language(val language: String) : SplitConfig(language)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Type] for an Apk containing assets.
|
||||||
|
*
|
||||||
|
* @param pixelDensity The target screen density.
|
||||||
|
*/
|
||||||
|
data class Asset(val pixelDensity: String) : SplitConfig(pixelDensity)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package app.revanced.patcher.apk
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkException
|
||||||
|
import app.revanced.arsc.resource.ResourceTable
|
||||||
|
import app.revanced.patcher.Patcher
|
||||||
|
import app.revanced.patcher.PatcherOptions
|
||||||
|
import app.revanced.patcher.apk.Apk.Companion.identify
|
||||||
|
import com.reandroid.apk.ApkModule
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [Apk] file of type [Apk.Split].
|
||||||
|
*
|
||||||
|
* @param files A list of apk files to load.
|
||||||
|
*/
|
||||||
|
class ApkBundle(files: List<File>) {
|
||||||
|
/**
|
||||||
|
* The [Apk.Base] of this [ApkBundle].
|
||||||
|
*/
|
||||||
|
val base: Apk.Base
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map containing all the [Apk.Split]s in this bundle associated by their configuration.
|
||||||
|
*/
|
||||||
|
val splits: Map<String, Apk.Split>?
|
||||||
|
|
||||||
|
init {
|
||||||
|
var baseApk: Apk.Base? = null
|
||||||
|
|
||||||
|
splits = buildMap {
|
||||||
|
files.forEach {
|
||||||
|
val apk = ApkModule.loadApkFile(it)
|
||||||
|
val (module, type) = apk.identify()
|
||||||
|
if (module is Apk.Module.DynamicFeature) {
|
||||||
|
return@forEach // Dynamic feature modules are not supported yet.
|
||||||
|
}
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
Apk.Type.Base -> {
|
||||||
|
if (baseApk != null) {
|
||||||
|
throw IllegalArgumentException("Cannot have more than one base apk")
|
||||||
|
}
|
||||||
|
baseApk = Apk.Base(apk)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Apk.Type.SplitConfig -> {
|
||||||
|
val target = type.target
|
||||||
|
if (this.contains(target)) {
|
||||||
|
throw IllegalArgumentException("Duplicate split: $target")
|
||||||
|
}
|
||||||
|
|
||||||
|
val constructor = when (type) {
|
||||||
|
is Apk.Type.Asset -> Apk.Split::Asset
|
||||||
|
is Apk.Type.Library -> Apk.Split::Library
|
||||||
|
is Apk.Type.Language -> Apk.Split::Language
|
||||||
|
}
|
||||||
|
|
||||||
|
this[target] = constructor(target, apk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
|
base = baseApk ?: throw IllegalArgumentException("Base apk not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Sequence] yielding all [Apk]s in this [ApkBundle].
|
||||||
|
*/
|
||||||
|
val all = sequence {
|
||||||
|
yield(base)
|
||||||
|
splits?.values?.let {
|
||||||
|
yieldAll(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the [app.revanced.arsc.resource.ResourceContainer] for the specified configuration.
|
||||||
|
*
|
||||||
|
* @param config The config to search for.
|
||||||
|
*/
|
||||||
|
fun query(config: String) = splits?.get(config)?.resources ?: base.resources
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh all updated resources in an [ApkBundle].
|
||||||
|
*
|
||||||
|
* @param options The [PatcherOptions] of the [Patcher].
|
||||||
|
* @return A sequence of the [Apk] files which are being refreshed.
|
||||||
|
*/
|
||||||
|
internal fun cleanup(options: PatcherOptions) = all.map {
|
||||||
|
var exception: ApkException? = null
|
||||||
|
try {
|
||||||
|
it.cleanup(options)
|
||||||
|
} catch (e: ApkException) {
|
||||||
|
exception = e
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitApkResult(it, exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [ResourceTable] of this [ApkBundle].
|
||||||
|
*/
|
||||||
|
val resources = ResourceTable(base.resources, all.map { it.resources })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of writing an [Apk] file.
|
||||||
|
*
|
||||||
|
* @param apk The corresponding [Apk] file.
|
||||||
|
* @param exception The optional [ApkException] when an exception occurred.
|
||||||
|
*/
|
||||||
|
data class SplitApkResult(val apk: Apk, val exception: ApkException? = null)
|
||||||
|
}
|
|
@ -4,10 +4,10 @@ import app.revanced.patcher.annotation.Compatibility
|
||||||
import app.revanced.patcher.annotation.Description
|
import app.revanced.patcher.annotation.Description
|
||||||
import app.revanced.patcher.annotation.Name
|
import app.revanced.patcher.annotation.Name
|
||||||
import app.revanced.patcher.annotation.Version
|
import app.revanced.patcher.annotation.Version
|
||||||
import app.revanced.patcher.data.Context
|
|
||||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||||
import app.revanced.patcher.patch.OptionsContainer
|
import app.revanced.patcher.patch.OptionsContainer
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchClass
|
||||||
import app.revanced.patcher.patch.PatchOptions
|
import app.revanced.patcher.patch.PatchOptions
|
||||||
import app.revanced.patcher.patch.annotations.DependsOn
|
import app.revanced.patcher.patch.annotations.DependsOn
|
||||||
import app.revanced.patcher.patch.annotations.RequiresIntegrations
|
import app.revanced.patcher.patch.annotations.RequiresIntegrations
|
||||||
|
@ -19,49 +19,49 @@ object PatchExtensions {
|
||||||
/**
|
/**
|
||||||
* The name of a [Patch].
|
* The name of a [Patch].
|
||||||
*/
|
*/
|
||||||
val Class<out Patch<Context>>.patchName: String
|
val PatchClass.patchName: String
|
||||||
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
|
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of a [Patch].
|
* The version of a [Patch].
|
||||||
*/
|
*/
|
||||||
val Class<out Patch<Context>>.version
|
val PatchClass.version
|
||||||
get() = findAnnotationRecursively(Version::class)?.version
|
get() = findAnnotationRecursively(Version::class)?.version
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Weather or not a [Patch] should be included.
|
* Weather or not a [Patch] should be included.
|
||||||
*/
|
*/
|
||||||
val Class<out Patch<Context>>.include
|
val PatchClass.include
|
||||||
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The description of a [Patch].
|
* The description of a [Patch].
|
||||||
*/
|
*/
|
||||||
val Class<out Patch<Context>>.description
|
val PatchClass.description
|
||||||
get() = findAnnotationRecursively(Description::class)?.description
|
get() = findAnnotationRecursively(Description::class)?.description
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The dependencies of a [Patch].
|
* The dependencies of a [Patch].
|
||||||
*/
|
*/
|
||||||
val Class<out Patch<Context>>.dependencies
|
val PatchClass.dependencies
|
||||||
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
|
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The packages a [Patch] is compatible with.
|
* The packages a [Patch] is compatible with.
|
||||||
*/
|
*/
|
||||||
val Class<out Patch<Context>>.compatiblePackages
|
val PatchClass.compatiblePackages
|
||||||
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
|
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Weather or not a [Patch] requires integrations.
|
* Weather or not a [Patch] requires integrations.
|
||||||
*/
|
*/
|
||||||
internal val Class<out Patch<Context>>.requiresIntegrations
|
internal val PatchClass.requiresIntegrations
|
||||||
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
|
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options of a [Patch].
|
* The options of a [Patch].
|
||||||
*/
|
*/
|
||||||
val Class<out Patch<Context>>.options: PatchOptions?
|
val PatchClass.options: PatchOptions?
|
||||||
get() = kotlin.companionObject?.let { cl ->
|
get() = kotlin.companionObject?.let { cl ->
|
||||||
if (cl.visibility != KVisibility.PUBLIC) return null
|
if (cl.visibility != KVisibility.PUBLIC) return null
|
||||||
kotlin.companionObjectInstance?.let {
|
kotlin.companionObjectInstance?.let {
|
|
@ -1,10 +1,10 @@
|
||||||
package app.revanced.patcher.fingerprint.method.impl
|
package app.revanced.patcher.fingerprint.method.impl
|
||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
import app.revanced.patcher.BytecodeContext
|
||||||
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
|
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
|
||||||
import app.revanced.patcher.fingerprint.Fingerprint
|
import app.revanced.patcher.fingerprint.Fingerprint
|
||||||
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||||
import app.revanced.patcher.patch.PatchResultError
|
import app.revanced.patcher.patch.PatchException
|
||||||
import app.revanced.patcher.util.proxy.ClassProxy
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
import org.jf.dexlib2.AccessFlags
|
import org.jf.dexlib2.AccessFlags
|
||||||
import org.jf.dexlib2.Opcode
|
import org.jf.dexlib2.Opcode
|
||||||
|
@ -99,9 +99,9 @@ abstract class MethodFingerprint(
|
||||||
methodClassPairs!!.add(methodClassPair)
|
methodClassPairs!!.add(methodClassPair)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (methods.isNotEmpty()) throw PatchResultError("Map already initialized")
|
if (methods.isNotEmpty()) throw PatchException("Map already initialized")
|
||||||
|
|
||||||
context.classes.classes.forEach { classDef ->
|
context.classes.forEach { classDef ->
|
||||||
classDef.methods.forEach { method ->
|
classDef.methods.forEach { method ->
|
||||||
val methodClassPair = method to classDef
|
val methodClassPair = method to classDef
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ abstract class MethodFingerprint(
|
||||||
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
|
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
|
||||||
*/
|
*/
|
||||||
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
|
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
|
||||||
if (methods.isEmpty()) throw PatchResultError("lookup map not initialized")
|
if (methods.isEmpty()) throw PatchException("lookup map not initialized")
|
||||||
|
|
||||||
for (fingerprint in this) {
|
for (fingerprint in this) {
|
||||||
fingerprint.resolveUsingLookupMap(context)
|
fingerprint.resolveUsingLookupMap(context)
|
||||||
|
@ -362,18 +362,18 @@ abstract class MethodFingerprint(
|
||||||
val patternOpcode = pattern.elementAt(patternIndex)
|
val patternOpcode = pattern.elementAt(patternIndex)
|
||||||
|
|
||||||
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
|
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
|
||||||
// reaching maximum threshold (0) means,
|
// Reaching maximum threshold (0) means,
|
||||||
// the pattern does not match to the current instructions
|
// the pattern does not match to the current instructions.
|
||||||
if (threshold-- == 0) break
|
if (threshold-- == 0) break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patternIndex < patternLength - 1) {
|
if (patternIndex < patternLength - 1) {
|
||||||
// if the entire pattern has not been scanned yet
|
// If the entire pattern has not been scanned yet
|
||||||
// continue the scan
|
// continue the scan.
|
||||||
patternIndex++
|
patternIndex++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
|
// The pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
|
||||||
val result =
|
val result =
|
||||||
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
|
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
|
||||||
index,
|
index,
|
||||||
|
@ -448,7 +448,7 @@ data class MethodFingerprintResult(
|
||||||
* Use [classDef] where possible.
|
* Use [classDef] where possible.
|
||||||
*/
|
*/
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
val mutableClass by lazy { context.classes.proxy(classDef).mutableClass }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a mutable clone of [method]
|
* Returns a mutable clone of [method]
|
|
@ -0,0 +1,20 @@
|
||||||
|
package app.revanced.patcher.logging
|
||||||
|
|
||||||
|
interface Logger {
|
||||||
|
fun error(msg: String) {}
|
||||||
|
fun warn(msg: String) {}
|
||||||
|
fun info(msg: String) {}
|
||||||
|
fun trace(msg: String) {}
|
||||||
|
|
||||||
|
object Nop : Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn a Patcher [Logger] into an [app.revanced.arsc.logging.Logger].
|
||||||
|
*/
|
||||||
|
internal fun Logger.asArscLogger() = object : app.revanced.arsc.logging.Logger {
|
||||||
|
override fun error(msg: String) = this@asArscLogger.error(msg)
|
||||||
|
override fun warn(msg: String) = this@asArscLogger.warn(msg)
|
||||||
|
override fun info(msg: String) = this@asArscLogger.info(msg)
|
||||||
|
override fun trace(msg: String) = this@asArscLogger.error(msg)
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
import app.revanced.patcher.BytecodeContext
|
||||||
import app.revanced.patcher.data.Context
|
import app.revanced.patcher.Context
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.ResourceContext
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
|
||||||
|
@ -17,9 +17,8 @@ sealed interface Patch<out T : Context> {
|
||||||
* The main function of the [Patch] which the patcher will call.
|
* The main function of the [Patch] which the patcher will call.
|
||||||
*
|
*
|
||||||
* @param context The [Context] the patch will work on.
|
* @param context The [Context] the patch will work on.
|
||||||
* @return The result of executing the patch.
|
|
||||||
*/
|
*/
|
||||||
fun execute(context: @UnsafeVariance T): PatchResult
|
suspend fun execute(context: @UnsafeVariance T)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,3 +34,9 @@ interface ResourcePatch : Patch<ResourceContext>
|
||||||
abstract class BytecodePatch(
|
abstract class BytecodePatch(
|
||||||
internal val fingerprints: Iterable<MethodFingerprint>? = null
|
internal val fingerprints: Iterable<MethodFingerprint>? = null
|
||||||
) : Patch<BytecodeContext>
|
) : Patch<BytecodeContext>
|
||||||
|
|
||||||
|
// TODO: populate this everywhere where the alias is not used yet
|
||||||
|
/**
|
||||||
|
* The class type of [Patch].
|
||||||
|
*/
|
||||||
|
typealias PatchClass = Class<out Patch<Context>>
|
|
@ -0,0 +1,12 @@
|
||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when patching.
|
||||||
|
*
|
||||||
|
* @param errorMessage The exception message.
|
||||||
|
* @param cause The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
|
||||||
|
constructor(errorMessage: String) : this(errorMessage, null)
|
||||||
|
constructor(cause: Throwable) : this(cause.message, cause)
|
||||||
|
}
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import java.nio.file.Path
|
|
||||||
import kotlin.io.path.pathString
|
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
class NoSuchOptionException(val option: String) : Exception("No such option: $option")
|
class NoSuchOptionException(val option: String) : Exception("No such option: $option")
|
|
@ -1,6 +1,6 @@
|
||||||
package app.revanced.patcher.patch.annotations
|
package app.revanced.patcher.patch.annotations
|
||||||
|
|
||||||
import app.revanced.patcher.data.Context
|
import app.revanced.patcher.Context
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ annotation class Patch(val include: Boolean = true)
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class DependsOn(
|
annotation class DependsOn(
|
||||||
val dependencies: Array<KClass<out Patch<Context>>> = []
|
val dependencies: Array<KClass<out Patch<Context>>> = [] // TODO: This should be a list of PatchClass instead
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,4 +24,4 @@ annotation class DependsOn(
|
||||||
* Annotation to mark [Patch]es which depend on integrations.
|
* Annotation to mark [Patch]es which depend on integrations.
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class RequiresIntegrations // required because integrations are decoupled from patches
|
annotation class RequiresIntegrations // TODO: Remove this annotation and replace it with a proper system
|
|
@ -1,6 +1,6 @@
|
||||||
package app.revanced.patcher.util
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
import app.revanced.patcher.PatcherContext
|
import app.revanced.patcher.BytecodeContext
|
||||||
import app.revanced.patcher.extensions.or
|
import app.revanced.patcher.extensions.or
|
||||||
import app.revanced.patcher.logging.Logger
|
import app.revanced.patcher.logging.Logger
|
||||||
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
|
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
|
||||||
|
@ -32,7 +32,7 @@ internal object ClassMerger {
|
||||||
* @param context The context to traverse the class hierarchy in.
|
* @param context The context to traverse the class hierarchy in.
|
||||||
* @param logger A logger.
|
* @param logger A logger.
|
||||||
*/
|
*/
|
||||||
fun ClassDef.merge(otherClass: ClassDef, context: PatcherContext, logger: Logger? = null) = this
|
fun ClassDef.merge(otherClass: ClassDef, context: BytecodeContext, logger: Logger? = null) = this
|
||||||
//.fixFieldAccess(otherClass, logger)
|
//.fixFieldAccess(otherClass, logger)
|
||||||
//.fixMethodAccess(otherClass, logger)
|
//.fixMethodAccess(otherClass, logger)
|
||||||
.addMissingFields(otherClass, logger)
|
.addMissingFields(otherClass, logger)
|
||||||
|
@ -89,10 +89,10 @@ internal object ClassMerger {
|
||||||
* @param context The context to traverse the class hierarchy in.
|
* @param context The context to traverse the class hierarchy in.
|
||||||
* @param logger A logger.
|
* @param logger A logger.
|
||||||
*/
|
*/
|
||||||
private fun ClassDef.publicize(reference: ClassDef, context: PatcherContext, logger: Logger? = null) =
|
private fun ClassDef.publicize(reference: ClassDef, context: BytecodeContext, logger: Logger? = null) =
|
||||||
if (reference.accessFlags.isPublic() && !accessFlags.isPublic())
|
if (reference.accessFlags.isPublic() && !accessFlags.isPublic())
|
||||||
this.asMutableClass().apply {
|
this.asMutableClass().apply {
|
||||||
context.bytecodeContext.traverseClassHierarchy(this) {
|
context.traverseClassHierarchy(this) {
|
||||||
if (accessFlags.isPublic()) return@traverseClassHierarchy
|
if (accessFlags.isPublic()) return@traverseClassHierarchy
|
||||||
|
|
||||||
logger?.trace("Publicizing ${this.type}")
|
logger?.trace("Publicizing ${this.type}")
|
|
@ -0,0 +1,89 @@
|
||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that represents a list of classes and proxies.
|
||||||
|
*
|
||||||
|
* @param classes The classes to be backed by proxies.
|
||||||
|
*/
|
||||||
|
class ProxyBackedClassList(classes: Set<ClassDef>) : Iterable<ClassDef> {
|
||||||
|
// A list for pending proxied classes to be added to the current ProxyBackedClassList instance.
|
||||||
|
private val proxiedClasses = mutableListOf<ClassProxy>()
|
||||||
|
private val mutableClasses = classes.toMutableList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the [mutableClasses]es with their proxies.
|
||||||
|
*/
|
||||||
|
internal fun applyProxies() {
|
||||||
|
proxiedClasses.removeIf { proxy ->
|
||||||
|
// If the proxy is unused, keep it in the proxiedClasses list.
|
||||||
|
if (!proxy.resolved) return@removeIf false
|
||||||
|
|
||||||
|
with(mutableClasses) {
|
||||||
|
remove(proxy.immutableClass)
|
||||||
|
add(proxy.mutableClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
return@removeIf true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace a [ClassDef] at a given [index].
|
||||||
|
*
|
||||||
|
* @param index The index of the class to be replaced.
|
||||||
|
* @param classDef The new class to replace the old one.
|
||||||
|
*/
|
||||||
|
operator fun set(index: Int, classDef: ClassDef) {
|
||||||
|
mutableClasses[index] = classDef
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [ClassDef] at a given [index].
|
||||||
|
*
|
||||||
|
* @param index The index of the class.
|
||||||
|
*/
|
||||||
|
operator fun get(index: Int) = mutableClasses[index]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator for the classes in [ProxyBackedClassList].
|
||||||
|
*
|
||||||
|
* @return The iterator for the classes.
|
||||||
|
*/
|
||||||
|
override fun iterator() = mutableClasses.iterator()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proxy a [ClassDef].
|
||||||
|
*
|
||||||
|
* Note: This creates a [ClassProxy] of the [ClassDef], if not already present.
|
||||||
|
*
|
||||||
|
* @return A proxy for the given class.
|
||||||
|
*/
|
||||||
|
fun proxy(classDef: ClassDef) = proxiedClasses
|
||||||
|
.find { it.immutableClass.type == classDef.type } ?: ClassProxy(classDef).also(proxiedClasses::add)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a [ClassDef].
|
||||||
|
*/
|
||||||
|
fun add(classDef: ClassDef) = mutableClasses.add(classDef)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a class by a given class name.
|
||||||
|
*
|
||||||
|
* @param className The name of the class.
|
||||||
|
* @return A proxy for the first class that matches the class name.
|
||||||
|
*/
|
||||||
|
fun findClassProxied(className: String) = findClassProxied { it.type.contains(className) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a class by a given predicate.
|
||||||
|
*
|
||||||
|
* @param predicate A predicate to match the class.
|
||||||
|
* @return A proxy for the first class that matches the predicate.
|
||||||
|
*/
|
||||||
|
fun findClassProxied(predicate: (ClassDef) -> Boolean) = this.find(predicate)?.let(::proxy)
|
||||||
|
|
||||||
|
val size get() = mutableClasses.size
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import app.revanced.patcher.BytecodeContext
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||||
|
|
||||||
|
object TypeUtil {
|
||||||
|
/**
|
||||||
|
* Traverse the class hierarchy starting from the given root class.
|
||||||
|
*
|
||||||
|
* @param targetClass The class to start traversing the class hierarchy from.
|
||||||
|
* @param callback The function that is called for every class in the hierarchy.
|
||||||
|
*/
|
||||||
|
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
|
||||||
|
callback(targetClass)
|
||||||
|
this.classes.findClassProxied(targetClass.superclass ?: return)?.mutableClass?.let {
|
||||||
|
traverseClassHierarchy(it, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Deprecated("This class serves no purpose anymore")
|
||||||
|
internal object VersionReader {
|
||||||
|
@JvmStatic
|
||||||
|
private val properties = Properties().apply {
|
||||||
|
load(
|
||||||
|
VersionReader::class.java.getResourceAsStream("/app/revanced/patcher/version.properties")
|
||||||
|
?: throw IllegalStateException("Could not load version.properties")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun read(): String {
|
||||||
|
return properties.getProperty("version") ?: throw IllegalStateException("Version not found")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package app.revanced.patcher.util.method
|
package app.revanced.patcher.util.method
|
||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
import app.revanced.patcher.BytecodeContext
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
import org.jf.dexlib2.iface.Method
|
import org.jf.dexlib2.iface.Method
|
||||||
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
|
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
|
@ -41,7 +41,7 @@ class MethodWalker internal constructor(
|
||||||
val instruction = instructions.elementAt(offset)
|
val instruction = instructions.elementAt(offset)
|
||||||
|
|
||||||
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
|
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
|
||||||
val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
|
val proxy = bytecodeContext.classes.findClassProxied(newMethod.definingClass)!!
|
||||||
|
|
||||||
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
|
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
|
||||||
currentMethod = methods.first {
|
currentMethod = methods.first {
|
|
@ -0,0 +1,59 @@
|
||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package app.revanced.patcher.util.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchClass
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import org.jf.dexlib2.DexFileFactory
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URLClassLoader
|
||||||
|
import java.util.jar.JarFile
|
||||||
|
import kotlin.streams.toList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A patch bundle.
|
||||||
|
*
|
||||||
|
* @param fromClasses The classes to get [Patch]es from.
|
||||||
|
*/
|
||||||
|
sealed class PatchBundle private constructor(fromClasses: Iterable<Class<*>>) : Iterable<PatchClass> {
|
||||||
|
private val patches = fromClasses.filter {
|
||||||
|
if (it.isAnnotation) return@filter false
|
||||||
|
|
||||||
|
it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null
|
||||||
|
}.map {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
it as PatchClass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun iterator() = patches.iterator()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A patch bundle of type [Jar].
|
||||||
|
*
|
||||||
|
* @param patchBundlePath The path to a patch bundle.
|
||||||
|
*/
|
||||||
|
class Jar(private val patchBundlePath: File) : PatchBundle(
|
||||||
|
with(URLClassLoader(arrayOf(patchBundlePath.toURI().toURL()), PatchBundle::class.java.classLoader)) {
|
||||||
|
JarFile(patchBundlePath).stream().filter { it.name.endsWith(".class") }.map {
|
||||||
|
loadClass(
|
||||||
|
it.realName.replace('/', '.').replace(".class", "")
|
||||||
|
)
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A patch bundle of type [Dex] format.
|
||||||
|
*
|
||||||
|
* @param patchBundlePath The path to a patch bundle of dex format.
|
||||||
|
*/
|
||||||
|
class Dex(private val patchBundlePath: File) : PatchBundle(
|
||||||
|
with(PathClassLoader(patchBundlePath.absolutePath, null, PatchBundle::class.java.classLoader)) {
|
||||||
|
DexFileFactory.loadDexFile(patchBundlePath, null).classes.map { classDef ->
|
||||||
|
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
|
||||||
|
}.map { loadClass(it) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ class MutableMethod(method: Method) : Method, BaseMethodReference() {
|
||||||
private var accessFlags = method.accessFlags
|
private var accessFlags = method.accessFlags
|
||||||
private var returnType = method.returnType
|
private var returnType = method.returnType
|
||||||
|
|
||||||
// Create own mutable MethodImplementation (due to not being able to change members like register count)
|
// TODO: 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 _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } }
|
||||||
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
|
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 _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
|
|
@ -4,7 +4,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableAnnotation.Companion.
|
||||||
import org.jf.dexlib2.base.BaseMethodParameter
|
import org.jf.dexlib2.base.BaseMethodParameter
|
||||||
import org.jf.dexlib2.iface.MethodParameter
|
import org.jf.dexlib2.iface.MethodParameter
|
||||||
|
|
||||||
// TODO: finish overriding all members if necessary
|
// TODO: Finish overriding all members if necessary.
|
||||||
class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() {
|
class MutableMethodParameter(parameter: MethodParameter) : MethodParameter, BaseMethodParameter() {
|
||||||
private var type = parameter.type
|
private var type = parameter.type
|
||||||
private var name = parameter.name
|
private var name = parameter.name
|
|
@ -3,8 +3,6 @@ package app.revanced.patcher.patch
|
||||||
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
|
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
import kotlin.io.path.Path
|
|
||||||
import kotlin.io.path.pathString
|
|
||||||
import kotlin.test.assertNotEquals
|
import kotlin.test.assertNotEquals
|
||||||
|
|
||||||
internal class PatchOptionsTest {
|
internal class PatchOptionsTest {
|
||||||
|
@ -35,10 +33,6 @@ internal class PatchOptionsTest {
|
||||||
println(choice)
|
println(choice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is PatchOption.PathOption -> {
|
|
||||||
option.value = Path("test.txt").pathString
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val option = options.get<String>("key1")
|
val option = options.get<String>("key1")
|
|
@ -1,13 +1,15 @@
|
||||||
package app.revanced.patcher.usage.bytecode
|
package app.revanced.patcher.usage.bytecode
|
||||||
|
|
||||||
|
import app.revanced.patcher.BytecodeContext
|
||||||
import app.revanced.patcher.annotation.Description
|
import app.revanced.patcher.annotation.Description
|
||||||
import app.revanced.patcher.annotation.Name
|
import app.revanced.patcher.annotation.Name
|
||||||
import app.revanced.patcher.annotation.Version
|
import app.revanced.patcher.annotation.Version
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||||
import app.revanced.patcher.extensions.or
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
import app.revanced.patcher.patch.*
|
import app.revanced.patcher.extensions.or
|
||||||
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.OptionsContainer
|
||||||
|
import app.revanced.patcher.patch.PatchOption
|
||||||
import app.revanced.patcher.patch.annotations.DependsOn
|
import app.revanced.patcher.patch.annotations.DependsOn
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
||||||
|
@ -29,7 +31,6 @@ import org.jf.dexlib2.immutable.reference.ImmutableFieldReference
|
||||||
import org.jf.dexlib2.immutable.reference.ImmutableStringReference
|
import org.jf.dexlib2.immutable.reference.ImmutableStringReference
|
||||||
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
||||||
import org.jf.dexlib2.util.Preconditions
|
import org.jf.dexlib2.util.Preconditions
|
||||||
import kotlin.io.path.Path
|
|
||||||
|
|
||||||
@Patch
|
@Patch
|
||||||
@Name("example-bytecode-patch")
|
@Name("example-bytecode-patch")
|
||||||
|
@ -40,7 +41,7 @@ import kotlin.io.path.Path
|
||||||
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
||||||
// This function will be executed by the patcher.
|
// This function will be executed by the patcher.
|
||||||
// You can treat it as a constructor
|
// You can treat it as a constructor
|
||||||
override fun execute(context: BytecodeContext): PatchResult {
|
override suspend fun execute(context: BytecodeContext) {
|
||||||
// Get the resolved method by its fingerprint from the resolver cache
|
// Get the resolved method by its fingerprint from the resolver cache
|
||||||
val result = ExampleFingerprint.result!!
|
val result = ExampleFingerprint.result!!
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
||||||
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
|
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
|
||||||
|
|
||||||
// Get the class in which the method matching our fingerprint is defined in.
|
// Get the class in which the method matching our fingerprint is defined in.
|
||||||
val mainClass = context.findClass {
|
val mainClass = context.classes.findClassProxied {
|
||||||
it.type == result.classDef.type
|
it.type == result.classDef.type
|
||||||
}!!.mutableClass
|
}!!.mutableClass
|
||||||
|
|
||||||
|
@ -127,12 +128,6 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
||||||
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
// Finally, tell the patcher that this patch was a success.
|
|
||||||
// You can also return PatchResultError with a message.
|
|
||||||
// If an exception is thrown inside this function,
|
|
||||||
// a PatchResultError will be returned with the error message.
|
|
||||||
return PatchResultSuccess()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -193,10 +188,5 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
|
||||||
"key5", null, "title", "description", true
|
"key5", null, "title", "description", true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
private var key6 by option(
|
|
||||||
PatchOption.PathOption(
|
|
||||||
"key6", Path("test.txt"), "title", "description", true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
package app.revanced.patcher.usage.resource.patch
|
package app.revanced.patcher.usage.resource.patch
|
||||||
|
|
||||||
|
import app.revanced.patcher.ResourceContext
|
||||||
import app.revanced.patcher.annotation.Description
|
import app.revanced.patcher.annotation.Description
|
||||||
import app.revanced.patcher.annotation.Name
|
import app.revanced.patcher.annotation.Name
|
||||||
import app.revanced.patcher.annotation.Version
|
import app.revanced.patcher.annotation.Version
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.apk.Apk
|
||||||
import app.revanced.patcher.patch.PatchResult
|
import app.revanced.patcher.openXmlFile
|
||||||
import app.revanced.patcher.patch.PatchResultSuccess
|
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
|
||||||
|
@ -17,8 +17,8 @@ import org.w3c.dom.Element
|
||||||
@ExampleResourceCompatibility
|
@ExampleResourceCompatibility
|
||||||
@Version("0.0.1")
|
@Version("0.0.1")
|
||||||
class ExampleResourcePatch : ResourcePatch {
|
class ExampleResourcePatch : ResourcePatch {
|
||||||
override fun execute(context: ResourceContext): PatchResult {
|
override suspend fun execute(context: ResourceContext) {
|
||||||
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
context.apkBundle.base.resources.openXmlFile(Apk.manifest).use { editor ->
|
||||||
val element = editor // regular DomFileEditor
|
val element = editor // regular DomFileEditor
|
||||||
.file
|
.file
|
||||||
.getElementsByTagName("application")
|
.getElementsByTagName("application")
|
||||||
|
@ -29,7 +29,5 @@ class ExampleResourcePatch : ResourcePatch {
|
||||||
"exampleValue"
|
"exampleValue"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PatchResultSuccess()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1 +1,29 @@
|
||||||
rootProject.name = "revanced-patcher"
|
rootProject.name = "ReVanced Patcher"
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||||
|
credentials {
|
||||||
|
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
|
||||||
|
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||||
|
credentials {
|
||||||
|
username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
|
||||||
|
password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
include("revanced-patcher", "arsclib-utils")
|
||||||
|
|
|
@ -1,419 +0,0 @@
|
||||||
package app.revanced.patcher
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.Context
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
|
|
||||||
import app.revanced.patcher.patch.*
|
|
||||||
import app.revanced.patcher.util.VersionReader
|
|
||||||
import brut.androlib.Androlib
|
|
||||||
import brut.androlib.meta.UsesFramework
|
|
||||||
import brut.androlib.options.BuildOptions
|
|
||||||
import brut.androlib.res.AndrolibResources
|
|
||||||
import brut.androlib.res.data.ResPackage
|
|
||||||
import brut.androlib.res.decoder.AXmlResourceParser
|
|
||||||
import brut.androlib.res.decoder.ResAttrDecoder
|
|
||||||
import brut.androlib.res.decoder.XmlPullStreamDecoder
|
|
||||||
import brut.androlib.res.xml.ResXmlPatcher
|
|
||||||
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.DexFile
|
|
||||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.nio.file.Files
|
|
||||||
|
|
||||||
internal val NAMER = BasicDexFileNamer()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ReVanced Patcher.
|
|
||||||
* @param options The options for the patcher.
|
|
||||||
*/
|
|
||||||
class Patcher(private val options: PatcherOptions) {
|
|
||||||
private val logger = options.logger
|
|
||||||
private val opcodes: Opcodes
|
|
||||||
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
|
|
||||||
private var mergeIntegrations = false
|
|
||||||
val context: PatcherContext
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
val version = VersionReader.read()
|
|
||||||
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
|
|
||||||
this.aaptPath = options.aaptPath
|
|
||||||
this.useAapt2 = true
|
|
||||||
this.frameworkFolderLocation = options.frameworkFolderLocation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
logger.info("Reading dex files")
|
|
||||||
// read dex files
|
|
||||||
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
|
||||||
// get the opcodes
|
|
||||||
opcodes = dexFile.opcodes
|
|
||||||
// finally create patcher context
|
|
||||||
context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
|
|
||||||
|
|
||||||
// decode manifest file
|
|
||||||
decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add integrations to be merged by the patcher.
|
|
||||||
* The integrations will only be merged, if necessary.
|
|
||||||
*
|
|
||||||
* @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files.
|
|
||||||
* @param callback The callback for [integrations] which are being added.
|
|
||||||
*/
|
|
||||||
fun addIntegrations(
|
|
||||||
integrations: List<File>,
|
|
||||||
callback: (File) -> Unit
|
|
||||||
) {
|
|
||||||
context.integrations.apply integrations@{
|
|
||||||
add(integrations)
|
|
||||||
this@integrations.callback = callback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the patched dex file.
|
|
||||||
*/
|
|
||||||
fun save(): PatcherResult {
|
|
||||||
val packageMetadata = context.packageMetadata
|
|
||||||
val metaInfo = packageMetadata.metaInfo
|
|
||||||
var resourceFile: File? = null
|
|
||||||
|
|
||||||
when (resourceDecodingMode) {
|
|
||||||
ResourceDecodingMode.FULL -> {
|
|
||||||
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
|
|
||||||
try {
|
|
||||||
val androlibResources = AndrolibResources().also { resources ->
|
|
||||||
resources.buildOptions = BuildOptions().also { buildOptions ->
|
|
||||||
buildOptions.setBuildOptions(options)
|
|
||||||
buildOptions.isFramework = metaInfo.isFrameworkApk
|
|
||||||
buildOptions.resourcesAreCompressed = metaInfo.compressionType
|
|
||||||
buildOptions.doNotCompress = metaInfo.doNotCompress
|
|
||||||
}
|
|
||||||
|
|
||||||
resources.setSdkInfo(metaInfo.sdkInfo)
|
|
||||||
resources.setVersionInfo(metaInfo.versionInfo)
|
|
||||||
resources.setSharedLibrary(metaInfo.sharedLibrary)
|
|
||||||
resources.setSparseResources(metaInfo.sparseResources)
|
|
||||||
}
|
|
||||||
|
|
||||||
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
|
|
||||||
|
|
||||||
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
|
|
||||||
|
|
||||||
val aaptFile = cacheDirectory.resolve("aapt_temp_file")
|
|
||||||
|
|
||||||
// delete if it exists
|
|
||||||
Files.deleteIfExists(aaptFile.toPath())
|
|
||||||
|
|
||||||
val resDirectory = cacheDirectory.resolve("res")
|
|
||||||
val includedFiles = metaInfo.usesFramework.ids.map { id ->
|
|
||||||
androlibResources.getFrameworkApk(
|
|
||||||
id, metaInfo.usesFramework.tag
|
|
||||||
)
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
logger.info("Compiling resources")
|
|
||||||
androlibResources.aaptPackage(
|
|
||||||
aaptFile, manifestFile, resDirectory, null, null, includedFiles
|
|
||||||
)
|
|
||||||
|
|
||||||
resourceFile = aaptFile
|
|
||||||
} finally {
|
|
||||||
cacheDirectory.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> logger.info("Not compiling resources because resource patching is not required")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace("Creating new dex file")
|
|
||||||
val newDexFile = object : DexFile {
|
|
||||||
override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
|
|
||||||
override fun getOpcodes() = this@Patcher.opcodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// write modified dex files
|
|
||||||
logger.info("Writing modified dex files")
|
|
||||||
val dexFiles = mutableMapOf<String, MemoryDataStore>()
|
|
||||||
MultiDexIO.writeDexFile(
|
|
||||||
true, -1, // core count
|
|
||||||
dexFiles, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
|
|
||||||
)
|
|
||||||
|
|
||||||
return PatcherResult(
|
|
||||||
dexFiles.map {
|
|
||||||
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
|
|
||||||
},
|
|
||||||
metaInfo.doNotCompress?.toList(),
|
|
||||||
resourceFile
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add [Patch]es to the patcher.
|
|
||||||
* @param patches [Patch]es The patches to add.
|
|
||||||
*/
|
|
||||||
fun addPatches(patches: Iterable<Class<out Patch<Context>>>) {
|
|
||||||
/**
|
|
||||||
* Returns true if at least one patches or its dependencies matches the given predicate.
|
|
||||||
*/
|
|
||||||
fun Class<out Patch<Context>>.anyRecursively(predicate: (Class<out Patch<Context>>) -> Boolean): Boolean =
|
|
||||||
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
|
|
||||||
|
|
||||||
|
|
||||||
// Determine if resource patching is required.
|
|
||||||
for (patch in patches) {
|
|
||||||
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
|
|
||||||
resourceDecodingMode = ResourceDecodingMode.FULL
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if merging integrations is required.
|
|
||||||
for (patch in patches) {
|
|
||||||
if (patch.anyRecursively { it.requiresIntegrations }) {
|
|
||||||
mergeIntegrations = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.patches.addAll(patches)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode resources for the patcher.
|
|
||||||
*
|
|
||||||
* @param mode The [ResourceDecodingMode] to use when decoding.
|
|
||||||
*/
|
|
||||||
private fun decodeResources(mode: ResourceDecodingMode) {
|
|
||||||
val extInputFile = ExtFile(options.inputFile)
|
|
||||||
try {
|
|
||||||
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
|
|
||||||
val resourceTable = androlib.getResTable(extInputFile, true)
|
|
||||||
when (mode) {
|
|
||||||
ResourceDecodingMode.FULL -> {
|
|
||||||
val outDir = File(options.resourceCacheDirectory)
|
|
||||||
if (outDir.exists()) {
|
|
||||||
logger.info("Deleting existing resource cache directory")
|
|
||||||
if (!outDir.deleteRecursively()) {
|
|
||||||
logger.error("Failed to delete existing resource cache directory")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outDir.mkdirs()
|
|
||||||
|
|
||||||
logger.info("Decoding resources")
|
|
||||||
|
|
||||||
// decode resources to cache directory
|
|
||||||
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
|
|
||||||
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
|
|
||||||
|
|
||||||
// read additional metadata from the resource table
|
|
||||||
context.packageMetadata.let { metadata ->
|
|
||||||
metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
|
|
||||||
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
|
|
||||||
}
|
|
||||||
|
|
||||||
// read files to not compress
|
|
||||||
metadata.metaInfo.doNotCompress = buildList {
|
|
||||||
androlib.recordUncompressedFiles(extInputFile, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ResourceDecodingMode.MANIFEST_ONLY -> {
|
|
||||||
logger.info("Decoding AndroidManifest.xml only, because resources are not needed")
|
|
||||||
|
|
||||||
// create decoder for the resource table
|
|
||||||
val decoder = ResAttrDecoder()
|
|
||||||
decoder.currentPackage = ResPackage(resourceTable, 0, null)
|
|
||||||
|
|
||||||
// create xml parser with the decoder
|
|
||||||
val axmlParser = AXmlResourceParser()
|
|
||||||
axmlParser.attrDecoder = decoder
|
|
||||||
|
|
||||||
// parse package information with the decoder and parser which will set required values in the resource table
|
|
||||||
// instead of decodeManifest another more low level solution can be created to make it faster/better
|
|
||||||
XmlPullStreamDecoder(
|
|
||||||
axmlParser, AndrolibResources().resXmlSerializer
|
|
||||||
).decodeManifest(
|
|
||||||
extInputFile.directory.getFileInput("AndroidManifest.xml"),
|
|
||||||
// Older Android versions do not support OutputStream.nullOutputStream()
|
|
||||||
object : OutputStream() {
|
|
||||||
override fun write(b: Int) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read of the resourceTable which is created by reading the manifest file
|
|
||||||
context.packageMetadata.let { metadata ->
|
|
||||||
metadata.packageName = resourceTable.currentResPackage.name
|
|
||||||
metadata.packageVersion = resourceTable.versionInfo.versionName ?: resourceTable.versionInfo.versionCode
|
|
||||||
metadata.metaInfo.versionInfo = resourceTable.versionInfo
|
|
||||||
metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
extInputFile.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute patches added the patcher.
|
|
||||||
*
|
|
||||||
* @param stopOnError If true, the patches will stop on the first error.
|
|
||||||
* @return A pair of the name of the [Patch] and its [PatchResult].
|
|
||||||
*/
|
|
||||||
fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
|
|
||||||
/**
|
|
||||||
* Execute a [Patch] and its dependencies recursively.
|
|
||||||
*
|
|
||||||
* @param patchClass The [Patch] to execute.
|
|
||||||
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
|
|
||||||
* @return The result of executing the [Patch].
|
|
||||||
*/
|
|
||||||
fun executePatch(
|
|
||||||
patchClass: Class<out Patch<Context>>,
|
|
||||||
executedPatches: LinkedHashMap<String, ExecutedPatch>
|
|
||||||
): PatchResult {
|
|
||||||
val patchName = patchClass.patchName
|
|
||||||
|
|
||||||
// if the patch has already applied silently skip it
|
|
||||||
if (executedPatches.contains(patchName)) {
|
|
||||||
if (!executedPatches[patchName]!!.success)
|
|
||||||
return PatchResultError("'$patchName' did not succeed previously")
|
|
||||||
|
|
||||||
logger.trace("Skipping '$patchName' because it has already been applied")
|
|
||||||
|
|
||||||
return PatchResultSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
// recursively execute all dependency patches
|
|
||||||
patchClass.dependencies?.forEach { dependencyClass ->
|
|
||||||
val dependency = dependencyClass.java
|
|
||||||
|
|
||||||
val result = executePatch(dependency, executedPatches)
|
|
||||||
if (result.isSuccess()) return@forEach
|
|
||||||
|
|
||||||
return PatchResultError(
|
|
||||||
"'$patchName' depends on '${dependency.patchName}' but the following error was raised: " +
|
|
||||||
result.error()!!.let { it.cause?.stackTraceToString() ?: it.message }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
|
|
||||||
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
|
|
||||||
|
|
||||||
// TODO: implement this in a more polymorphic way
|
|
||||||
val patchContext = if (isResourcePatch) {
|
|
||||||
context.resourceContext
|
|
||||||
} else {
|
|
||||||
context.bytecodeContext.also { context ->
|
|
||||||
(patchInstance as BytecodePatch).fingerprints?.resolveUsingLookupMap(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
|
|
||||||
|
|
||||||
return try {
|
|
||||||
patchInstance.execute(patchContext).also {
|
|
||||||
executedPatches[patchName] = ExecutedPatch(patchInstance, it.isSuccess())
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
PatchResultError(e).also {
|
|
||||||
executedPatches[patchName] = ExecutedPatch(patchInstance, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sequence {
|
|
||||||
if (mergeIntegrations) context.integrations.merge(logger)
|
|
||||||
|
|
||||||
logger.trace("Initialize lookup maps for method MethodFingerprint resolution")
|
|
||||||
|
|
||||||
MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext)
|
|
||||||
|
|
||||||
// prevent from decoding the manifest twice if it is not needed
|
|
||||||
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
|
|
||||||
|
|
||||||
logger.info("Executing patches")
|
|
||||||
|
|
||||||
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // first is name
|
|
||||||
|
|
||||||
context.patches.forEach { patch ->
|
|
||||||
val patchResult = executePatch(patch, executedPatches)
|
|
||||||
|
|
||||||
val result = if (patchResult.isSuccess()) {
|
|
||||||
Result.success(patchResult.success()!!)
|
|
||||||
} else {
|
|
||||||
Result.failure(patchResult.error()!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This prints before the patch really finishes in case it is a Closeable
|
|
||||||
// because the Closeable is closed after all patches are executed.
|
|
||||||
yield(patch.patchName to result)
|
|
||||||
|
|
||||||
if (stopOnError && patchResult.isError()) return@sequence
|
|
||||||
}
|
|
||||||
|
|
||||||
executedPatches.values
|
|
||||||
.filter(ExecutedPatch::success)
|
|
||||||
.map(ExecutedPatch::patchInstance)
|
|
||||||
.filterIsInstance(Closeable::class.java)
|
|
||||||
.asReversed().forEach {
|
|
||||||
try {
|
|
||||||
it.close()
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
val patchName = (it as Patch<Context>).javaClass.patchName
|
|
||||||
|
|
||||||
logger.error("Failed to close '$patchName': ${exception.stackTraceToString()}")
|
|
||||||
|
|
||||||
yield(patchName to Result.failure(exception))
|
|
||||||
|
|
||||||
// This is not failsafe. If a patch throws an exception while closing,
|
|
||||||
// the other patches that depend on it may fail.
|
|
||||||
if (stopOnError) return@sequence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MethodFingerprint.clearFingerprintResolutionLookupMaps()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of decoding the resources.
|
|
||||||
*/
|
|
||||||
private enum class ResourceDecodingMode {
|
|
||||||
/**
|
|
||||||
* Decode all resources.
|
|
||||||
*/
|
|
||||||
FULL,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode the manifest file only.
|
|
||||||
*/
|
|
||||||
MANIFEST_ONLY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A result of executing a [Patch].
|
|
||||||
*
|
|
||||||
* @param patchInstance The instance of the [Patch] that was applied.
|
|
||||||
* @param success The result of the [Patch].
|
|
||||||
*/
|
|
||||||
internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)
|
|
|
@ -1,64 +0,0 @@
|
||||||
package app.revanced.patcher
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.*
|
|
||||||
import app.revanced.patcher.logging.Logger
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
import app.revanced.patcher.util.ClassMerger.merge
|
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
data class PatcherContext(
|
|
||||||
val classes: MutableList<ClassDef>,
|
|
||||||
val resourceCacheDirectory: File,
|
|
||||||
) {
|
|
||||||
val packageMetadata = PackageMetadata()
|
|
||||||
internal val patches = mutableListOf<Class<out Patch<Context>>>()
|
|
||||||
internal val integrations = Integrations(this)
|
|
||||||
internal val bytecodeContext = BytecodeContext(classes)
|
|
||||||
internal val resourceContext = ResourceContext(resourceCacheDirectory)
|
|
||||||
|
|
||||||
internal class Integrations(val context: PatcherContext) {
|
|
||||||
var callback: ((File) -> Unit)? = null
|
|
||||||
private val integrations: MutableList<File> = mutableListOf()
|
|
||||||
|
|
||||||
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge integrations.
|
|
||||||
* @param logger A logger.
|
|
||||||
*/
|
|
||||||
fun merge(logger: Logger) {
|
|
||||||
with(context.bytecodeContext.classes) {
|
|
||||||
for (integrations in integrations) {
|
|
||||||
callback?.let { it(integrations) }
|
|
||||||
|
|
||||||
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile(
|
|
||||||
true,
|
|
||||||
integrations,
|
|
||||||
NAMER,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
).classes) {
|
|
||||||
val type = classDef.type
|
|
||||||
|
|
||||||
val result = classes.findIndexed { it.type == type }
|
|
||||||
if (result == null) {
|
|
||||||
logger.trace("Merging type $type")
|
|
||||||
classes.add(classDef)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val (existingClass, existingClassIndex) = result
|
|
||||||
|
|
||||||
logger.trace("Type $type exists. Adding missing methods and fields.")
|
|
||||||
|
|
||||||
existingClass.merge(classDef, context, logger).let { mergedClass ->
|
|
||||||
if (mergedClass !== existingClass) // referential equality check
|
|
||||||
classes[existingClassIndex] = mergedClass
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package app.revanced.patcher
|
|
||||||
|
|
||||||
import app.revanced.patcher.logging.Logger
|
|
||||||
import app.revanced.patcher.logging.impl.NopLogger
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for the [Patcher].
|
|
||||||
* @param inputFile The input file (usually an apk file).
|
|
||||||
* @param resourceCacheDirectory Directory to cache resources.
|
|
||||||
* @param aaptPath Optional path to a custom aapt binary.
|
|
||||||
* @param frameworkFolderLocation Optional path to a custom framework folder.
|
|
||||||
* @param logger Custom logger implementation for the [Patcher].
|
|
||||||
*/
|
|
||||||
data class PatcherOptions(
|
|
||||||
internal val inputFile: File,
|
|
||||||
internal val resourceCacheDirectory: String,
|
|
||||||
internal val aaptPath: String? = null,
|
|
||||||
internal val frameworkFolderLocation: String? = null,
|
|
||||||
internal val logger: Logger = NopLogger
|
|
||||||
)
|
|
|
@ -1,16 +0,0 @@
|
||||||
package app.revanced.patcher
|
|
||||||
|
|
||||||
import app.revanced.patcher.util.dex.DexFile
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The result of a patcher.
|
|
||||||
* @param dexFiles The patched dex files.
|
|
||||||
* @param doNotCompress List of relative paths to files to exclude from compressing.
|
|
||||||
* @param resourceFile File containing resources that need to be extracted into the APK.
|
|
||||||
*/
|
|
||||||
data class PatcherResult(
|
|
||||||
val dexFiles: List<DexFile>,
|
|
||||||
val doNotCompress: List<String>? = null,
|
|
||||||
val resourceFile: File?
|
|
||||||
)
|
|
|
@ -1,170 +0,0 @@
|
||||||
package app.revanced.patcher.data
|
|
||||||
|
|
||||||
import app.revanced.patcher.util.ProxyBackedClassList
|
|
||||||
import app.revanced.patcher.util.method.MethodWalker
|
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
|
||||||
import org.jf.dexlib2.iface.Method
|
|
||||||
import org.w3c.dom.Document
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
|
||||||
import javax.xml.transform.TransformerFactory
|
|
||||||
import javax.xml.transform.dom.DOMSource
|
|
||||||
import javax.xml.transform.stream.StreamResult
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext].
|
|
||||||
*/
|
|
||||||
|
|
||||||
sealed interface Context
|
|
||||||
|
|
||||||
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
|
|
||||||
/**
|
|
||||||
* The list of classes.
|
|
||||||
*/
|
|
||||||
val classes = ProxyBackedClassList(classes)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a class by a given class name.
|
|
||||||
*
|
|
||||||
* @param className The name of the class.
|
|
||||||
* @return A proxy for the first class that matches the class name.
|
|
||||||
*/
|
|
||||||
fun findClass(className: String) = findClass { it.type.contains(className) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a class by a given predicate.
|
|
||||||
*
|
|
||||||
* @param predicate A predicate to match the class.
|
|
||||||
* @return A proxy for the first class that matches the predicate.
|
|
||||||
*/
|
|
||||||
fun findClass(predicate: (ClassDef) -> Boolean) =
|
|
||||||
// if we already proxied the class matching the predicate...
|
|
||||||
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
|
|
||||||
// else resolve the class to a proxy and return it, if the predicate is matching a class
|
|
||||||
classes.find(predicate)?.let { proxy(it) }
|
|
||||||
|
|
||||||
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
|
|
||||||
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
|
|
||||||
if (proxy == null) {
|
|
||||||
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
|
|
||||||
this.classes.add(proxy)
|
|
||||||
}
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
|
||||||
*
|
|
||||||
* @param startMethod The method to start at.
|
|
||||||
* @return A [MethodWalker] instance.
|
|
||||||
*/
|
|
||||||
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
|
|
||||||
return MethodWalker(this, startMethod)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
|
|
||||||
for ((index, element) in this.withIndex()) {
|
|
||||||
if (predicate(element)) {
|
|
||||||
return element to index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
|
|
||||||
val xmlEditor = XmlFileHolder()
|
|
||||||
|
|
||||||
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
|
|
||||||
|
|
||||||
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
|
|
||||||
|
|
||||||
inner class XmlFileHolder {
|
|
||||||
operator fun get(inputStream: InputStream) =
|
|
||||||
DomFileEditor(inputStream)
|
|
||||||
|
|
||||||
operator fun get(path: String): DomFileEditor {
|
|
||||||
return DomFileEditor(this@ResourceContext[path])
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for a file that can be edited as a dom document.
|
|
||||||
*
|
|
||||||
* This constructor does not check for locks to the file when writing.
|
|
||||||
* Use the secondary constructor.
|
|
||||||
*
|
|
||||||
* @param inputStream the input stream to read the xml file from.
|
|
||||||
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
class DomFileEditor internal constructor(
|
|
||||||
private val inputStream: InputStream,
|
|
||||||
private val outputStream: Lazy<OutputStream>? = null,
|
|
||||||
) : Closeable {
|
|
||||||
// path to the xml file to unlock the resource when closing the editor
|
|
||||||
private var filePath: String? = null
|
|
||||||
private var closed: Boolean = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The document of the xml file
|
|
||||||
*/
|
|
||||||
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
|
||||||
.also(Document::normalize)
|
|
||||||
|
|
||||||
|
|
||||||
// lazily open an output stream
|
|
||||||
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
|
|
||||||
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
|
|
||||||
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
|
|
||||||
// increase the lock
|
|
||||||
locks.merge(file.path, 1, Integer::sum)
|
|
||||||
filePath = file.path
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the editor. Write backs and decreases the lock count.
|
|
||||||
*
|
|
||||||
* Will not write back to the file if the file is still locked.
|
|
||||||
*/
|
|
||||||
override fun close() {
|
|
||||||
if (closed) return
|
|
||||||
|
|
||||||
inputStream.close()
|
|
||||||
|
|
||||||
// if the output stream is not null, do not close it
|
|
||||||
outputStream?.let {
|
|
||||||
// prevent writing to same file, if it is being locked
|
|
||||||
// isLocked will be false if the editor was created through a stream
|
|
||||||
val isLocked = filePath?.let { path ->
|
|
||||||
val isLocked = locks[path]!! > 1
|
|
||||||
// decrease the lock count if the editor was opened for a file
|
|
||||||
locks.merge(path, -1, Integer::sum)
|
|
||||||
isLocked
|
|
||||||
} ?: false
|
|
||||||
|
|
||||||
// if unlocked, write back to the file
|
|
||||||
if (!isLocked) {
|
|
||||||
it.value.use { stream ->
|
|
||||||
val result = StreamResult(stream)
|
|
||||||
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.value.close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
// map of concurrent open files
|
|
||||||
val locks = mutableMapOf<String, Int>()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package app.revanced.patcher.logging
|
|
||||||
|
|
||||||
interface Logger {
|
|
||||||
fun error(msg: String) {}
|
|
||||||
fun warn(msg: String) {}
|
|
||||||
fun info(msg: String) {}
|
|
||||||
fun trace(msg: String) {}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package app.revanced.patcher.logging.impl
|
|
||||||
|
|
||||||
import app.revanced.patcher.logging.Logger
|
|
||||||
|
|
||||||
object NopLogger : Logger
|
|
|
@ -1,35 +0,0 @@
|
||||||
package app.revanced.patcher.patch
|
|
||||||
|
|
||||||
interface PatchResult {
|
|
||||||
fun error(): PatchResultError? {
|
|
||||||
if (this is PatchResultError) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun success(): PatchResultSuccess? {
|
|
||||||
if (this is PatchResultSuccess) {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isError(): Boolean {
|
|
||||||
return this is PatchResultError
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isSuccess(): Boolean {
|
|
||||||
return this is PatchResultSuccess
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PatchResultError(
|
|
||||||
errorMessage: String?, cause: Exception?
|
|
||||||
) : Exception(errorMessage, cause), PatchResult {
|
|
||||||
constructor(errorMessage: String) : this(errorMessage, null)
|
|
||||||
constructor(cause: Exception) : this(cause.message, cause)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class PatchResultSuccess : PatchResult
|
|
|
@ -1,46 +0,0 @@
|
||||||
package app.revanced.patcher.util
|
|
||||||
|
|
||||||
import app.revanced.patcher.util.proxy.ClassProxy
|
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class that represents a set of classes and proxies.
|
|
||||||
*
|
|
||||||
* @param classes The classes to be backed by proxies.
|
|
||||||
*/
|
|
||||||
class ProxyBackedClassList(internal val classes: MutableList<ClassDef>) : Set<ClassDef> {
|
|
||||||
internal val proxies = mutableListOf<ClassProxy>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a [ClassDef].
|
|
||||||
*/
|
|
||||||
fun add(classDef: ClassDef) = classes.add(classDef)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a [ClassProxy].
|
|
||||||
*/
|
|
||||||
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace all classes with their mutated versions.
|
|
||||||
*/
|
|
||||||
internal fun replaceClasses() =
|
|
||||||
proxies.removeIf { proxy ->
|
|
||||||
// if the proxy is unused, keep it in the list
|
|
||||||
if (!proxy.resolved) return@removeIf false
|
|
||||||
|
|
||||||
// if it has been used, replace the original class with the new class
|
|
||||||
val index = classes.indexOfFirst { it.type == proxy.immutableClass.type }
|
|
||||||
classes[index] = proxy.mutableClass
|
|
||||||
|
|
||||||
// return true to remove it from the proxies list
|
|
||||||
return@removeIf true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override val size get() = classes.size
|
|
||||||
override fun contains(element: ClassDef) = classes.contains(element)
|
|
||||||
override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
|
|
||||||
override fun isEmpty() = classes.isEmpty()
|
|
||||||
override fun iterator() = classes.iterator()
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package app.revanced.patcher.util
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
|
||||||
|
|
||||||
object TypeUtil {
|
|
||||||
/**
|
|
||||||
* traverse the class hierarchy starting from the given root class
|
|
||||||
*
|
|
||||||
* @param targetClass the class to start traversing the class hierarchy from
|
|
||||||
* @param callback function that is called for every class in the hierarchy
|
|
||||||
*/
|
|
||||||
fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
|
|
||||||
callback(targetClass)
|
|
||||||
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
|
|
||||||
traverseClassHierarchy(it, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package app.revanced.patcher.util
|
|
||||||
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
internal object VersionReader {
|
|
||||||
@JvmStatic
|
|
||||||
private val props = Properties().apply {
|
|
||||||
load(
|
|
||||||
VersionReader::class.java.getResourceAsStream("/revanced-patcher/version.properties")
|
|
||||||
?: throw IllegalStateException("Could not load version.properties")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun read(): String {
|
|
||||||
return props.getProperty("version") ?: throw IllegalStateException("Version not found")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package app.revanced.patcher.util.dex
|
|
||||||
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for dex files.
|
|
||||||
* @param name The original name of the dex file.
|
|
||||||
* @param stream The dex file as [InputStream].
|
|
||||||
*/
|
|
||||||
data class DexFile(val name: String, val stream: InputStream)
|
|
|
@ -1,76 +0,0 @@
|
||||||
@file:Suppress("unused")
|
|
||||||
|
|
||||||
package app.revanced.patcher.util.patch
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.Context
|
|
||||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
import org.jf.dexlib2.DexFileFactory
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URLClassLoader
|
|
||||||
import java.util.jar.JarFile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A patch bundle.
|
|
||||||
|
|
||||||
* @param path The path to the patch bundle.
|
|
||||||
*/
|
|
||||||
sealed class PatchBundle(path: String) : File(path) {
|
|
||||||
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
|
|
||||||
classNames.forEach { className ->
|
|
||||||
val clazz = classLoader.loadClass(className)
|
|
||||||
|
|
||||||
// Annotations can not Patch.
|
|
||||||
if (clazz.isAnnotation) return@forEach
|
|
||||||
|
|
||||||
clazz.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
|
|
||||||
?: return@forEach
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Context>>)
|
|
||||||
}
|
|
||||||
}.sortedBy { it.patchName }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A patch bundle of type [Jar].
|
|
||||||
*
|
|
||||||
* @param patchBundlePath The path to the patch bundle.
|
|
||||||
*/
|
|
||||||
class Jar(patchBundlePath: String) : PatchBundle(patchBundlePath) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load patches from the patch bundle.
|
|
||||||
*
|
|
||||||
* Patches will be loaded with a new [URLClassLoader].
|
|
||||||
*/
|
|
||||||
fun loadPatches() = loadPatches(
|
|
||||||
URLClassLoader(
|
|
||||||
arrayOf(this.toURI().toURL()),
|
|
||||||
Thread.currentThread().contextClassLoader // TODO: find out why this is required
|
|
||||||
),
|
|
||||||
JarFile(this)
|
|
||||||
.stream()
|
|
||||||
.filter { it.name.endsWith(".class") && !it.name.contains("$") }
|
|
||||||
.map { it.realName.replace('/', '.').replace(".class", "") }.iterator()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A patch bundle of type [Dex] format.
|
|
||||||
*
|
|
||||||
* @param patchBundlePath The path to a patch bundle of dex format.
|
|
||||||
* @param dexClassLoader The dex class loader.
|
|
||||||
*/
|
|
||||||
class Dex(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) {
|
|
||||||
/**
|
|
||||||
* Load patches from the patch bundle.
|
|
||||||
*
|
|
||||||
* Patches will be loaded to the provided [dexClassLoader].
|
|
||||||
*/
|
|
||||||
fun loadPatches() = loadPatches(dexClassLoader,
|
|
||||||
DexFileFactory.loadDexFile(path, null).classes.asSequence().map { classDef ->
|
|
||||||
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
|
|
||||||
}.iterator()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,233 +0,0 @@
|
||||||
package app.revanced.patcher.extensions
|
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
|
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
|
||||||
import app.revanced.patcher.util.smali.ExternalLabel
|
|
||||||
import org.jf.dexlib2.AccessFlags
|
|
||||||
import org.jf.dexlib2.Opcode
|
|
||||||
import org.jf.dexlib2.builder.BuilderOffsetInstruction
|
|
||||||
import org.jf.dexlib2.builder.MutableMethodImplementation
|
|
||||||
import org.jf.dexlib2.builder.instruction.BuilderInstruction21s
|
|
||||||
import org.jf.dexlib2.iface.instruction.OneRegisterInstruction
|
|
||||||
import org.jf.dexlib2.immutable.ImmutableMethod
|
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
private object InstructionExtensionsTest {
|
|
||||||
private lateinit var testMethod: MutableMethod
|
|
||||||
private lateinit var testMethodImplementation: MutableMethodImplementation
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
fun createTestMethod() = ImmutableMethod(
|
|
||||||
"TestClass;",
|
|
||||||
"testMethod",
|
|
||||||
null,
|
|
||||||
"V",
|
|
||||||
AccessFlags.PUBLIC.value,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
|
|
||||||
repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
|
|
||||||
},
|
|
||||||
).let { testMethod = it.toMutable() }
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addInstructionsToImplementationIndexed() = applyToImplementation {
|
|
||||||
addInstructions(5, getTestInstructions(5..6)).also {
|
|
||||||
assertRegisterIs(5, 5)
|
|
||||||
assertRegisterIs(6, 6)
|
|
||||||
|
|
||||||
assertRegisterIs(5, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addInstructionsToImplementation() = applyToImplementation {
|
|
||||||
addInstructions(getTestInstructions(10..11)).also {
|
|
||||||
assertRegisterIs(10, 10)
|
|
||||||
assertRegisterIs(11, 11)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun removeInstructionsFromImplementationIndexed() = applyToImplementation {
|
|
||||||
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun removeInstructionsFromImplementation() = applyToImplementation {
|
|
||||||
removeInstructions(0).also { assertRegisterIs(9, 9) }
|
|
||||||
removeInstructions(1).also { assertRegisterIs(1, 0) }
|
|
||||||
removeInstructions(2).also { assertRegisterIs(3, 0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun replaceInstructionsInImplementationIndexed() = applyToImplementation {
|
|
||||||
replaceInstructions(5, getTestInstructions(0..1)).also {
|
|
||||||
assertRegisterIs(0, 5)
|
|
||||||
assertRegisterIs(1, 6)
|
|
||||||
assertRegisterIs(7, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addInstructionToMethodIndexed() = applyToMethod {
|
|
||||||
addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addInstructionToMethod() = applyToMethod {
|
|
||||||
addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addSmaliInstructionToMethodIndexed() = applyToMethod {
|
|
||||||
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addSmaliInstructionToMethod() = applyToMethod {
|
|
||||||
addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addInstructionsToMethodIndexed() = applyToMethod {
|
|
||||||
addInstructions(5, getTestInstructions(0..1)).also {
|
|
||||||
assertRegisterIs(0, 5)
|
|
||||||
assertRegisterIs(1, 6)
|
|
||||||
|
|
||||||
assertRegisterIs(5, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addInstructionsToMethod() = applyToMethod {
|
|
||||||
addInstructions(getTestInstructions(0..1)).also {
|
|
||||||
assertRegisterIs(0, 10)
|
|
||||||
assertRegisterIs(1, 11)
|
|
||||||
|
|
||||||
assertRegisterIs(9, 9)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addSmaliInstructionsToMethodIndexed() = applyToMethod {
|
|
||||||
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
|
|
||||||
assertRegisterIs(0, 5)
|
|
||||||
assertRegisterIs(1, 6)
|
|
||||||
|
|
||||||
assertRegisterIs(5, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addSmaliInstructionsToMethod() = applyToMethod {
|
|
||||||
addInstructions(getTestSmaliInstructions(0..1)).also {
|
|
||||||
assertRegisterIs(0, 10)
|
|
||||||
assertRegisterIs(1, 11)
|
|
||||||
|
|
||||||
assertRegisterIs(9, 9)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() = applyToMethod {
|
|
||||||
val label = ExternalLabel("testLabel", getInstruction(5))
|
|
||||||
|
|
||||||
addInstructionsWithLabels(
|
|
||||||
5,
|
|
||||||
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
|
|
||||||
label
|
|
||||||
).also {
|
|
||||||
assertRegisterIs(0, 5)
|
|
||||||
assertRegisterIs(1, 6)
|
|
||||||
assertRegisterIs(5, 8)
|
|
||||||
|
|
||||||
val gotoTarget = getInstruction<BuilderOffsetInstruction>(7)
|
|
||||||
.target.location.instruction as OneRegisterInstruction
|
|
||||||
|
|
||||||
assertEquals(5, gotoTarget.registerA)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun removeInstructionFromMethodIndexed() = applyToMethod {
|
|
||||||
removeInstruction(5).also {
|
|
||||||
assertRegisterIs(4, 4)
|
|
||||||
assertRegisterIs(6, 5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun removeInstructionsFromMethodIndexed() = applyToMethod {
|
|
||||||
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun removeInstructionsFromMethod() = applyToMethod {
|
|
||||||
removeInstructions(0).also { assertRegisterIs(9, 9) }
|
|
||||||
removeInstructions(1).also { assertRegisterIs(1, 0) }
|
|
||||||
removeInstructions(2).also { assertRegisterIs(3, 0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun replaceInstructionInMethodIndexed() = applyToMethod {
|
|
||||||
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun replaceInstructionsInMethodIndexed() = applyToMethod {
|
|
||||||
replaceInstructions(5, getTestInstructions(0..1)).also {
|
|
||||||
assertRegisterIs(0, 5)
|
|
||||||
assertRegisterIs(1, 6)
|
|
||||||
assertRegisterIs(7, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun replaceSmaliInstructionsInMethodIndexed() = applyToMethod {
|
|
||||||
replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
|
|
||||||
assertRegisterIs(0, 5)
|
|
||||||
assertRegisterIs(1, 6)
|
|
||||||
assertRegisterIs(7, 7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Helper methods
|
|
||||||
|
|
||||||
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
|
|
||||||
testMethodImplementation.apply(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyToMethod(block: MutableMethod.() -> Unit) {
|
|
||||||
testMethod.apply(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MutableMethodImplementation.assertRegisterIs(register: Int, atIndex: Int) = assertEquals(
|
|
||||||
register, getInstruction<OneRegisterInstruction>(atIndex).registerA
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun MutableMethod.assertRegisterIs(register: Int, atIndex: Int) =
|
|
||||||
implementation!!.assertRegisterIs(register, atIndex)
|
|
||||||
|
|
||||||
private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) }
|
|
||||||
|
|
||||||
private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0"
|
|
||||||
|
|
||||||
private fun getTestSmaliInstructions(range: IntRange) = range.joinToString("\n") {
|
|
||||||
getTestSmaliInstruction(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
private class TestInstruction(register: Int) : BuilderInstruction21s(Opcode.CONST_16, register, 0)
|
|
||||||
}
|
|
Loading…
Reference in a new issue