1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-06 16:38:50 +02:00

Compare commits

..

9 Commits

Author SHA1 Message Date
oSumAtrIX
c6fdf97794 refactor: do initial refactoring attempt 2023-07-30 00:10:49 +02:00
oSumAtrIX
c52f0b80f2 feat: Deprecate Version annotation 2023-07-30 00:10:49 +02:00
oSumAtrIX
4b5e25b29c chore: naming and better exception message 2023-07-30 00:10:48 +02:00
oSumAtrIX
c752a3c596 chore: add/fix comments 2023-07-30 00:10:48 +02:00
oSumAtrIX
740911a2a3 refactor: move stuff around and improve memory profile/performance 2023-07-30 00:10:48 +02:00
oSumAtrIX
242d805c6c chore: do not publish library, instead shade 2023-07-30 00:10:47 +02:00
oSumAtrIX
c543fdc18b chore: remove unnecessary naming 2023-07-30 00:10:47 +02:00
oSumAtrIX
d48a8e697f arsclib 2023-07-30 00:10:47 +02:00
Palm
8749a61d39 feat: remove Path option (#202)
BREAKING CHANGE: This removes the previously available `Path` option
2023-07-06 18:17:51 +02:00
98 changed files with 2065 additions and 1467 deletions

View File

@@ -7,13 +7,7 @@
}
],
"plugins": [
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"gradle-semantic-release-plugin",

View File

@@ -1,89 +1,3 @@
# [13.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1...v13.0.0-dev.1) (2023-08-11)
* build(Needs bump)!: Bump dependencies ([d5f89a9](https://github.com/ReVanced/revanced-patcher/commit/d5f89a903f019c199bdb27a50287124fc4b4978e))
### BREAKING CHANGES
* This bump updates smali, a crucial dependency
## [12.1.1](https://github.com/ReVanced/revanced-patcher/compare/v12.1.0...v12.1.1) (2023-08-03)
### Bug Fixes
* clear method lookup maps before initializing them ([#210](https://github.com/ReVanced/revanced-patcher/issues/210)) ([746544f](https://github.com/ReVanced/revanced-patcher/commit/746544f9d51d1013bb160075709cd26bffd425b3))
## [12.1.1-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1-dev.1...v12.1.1-dev.2) (2023-08-03)
## [12.1.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v12.1.0...v12.1.1-dev.1) (2023-08-03)
### Bug Fixes
* clear method lookup maps before initializing them ([#210](https://github.com/ReVanced/revanced-patcher/issues/210)) ([746544f](https://github.com/ReVanced/revanced-patcher/commit/746544f9d51d1013bb160075709cd26bffd425b3))
# [12.1.0](https://github.com/ReVanced/revanced-patcher/compare/v12.0.0...v12.1.0) (2023-08-03)
### Features
* add `MutableMethod.getInstructions` extension function ([fae4029](https://github.com/ReVanced/revanced-patcher/commit/fae4029cfccfad7aa3dd8f7fbef1c63ee26b85b3))
# [12.1.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v12.1.0-dev.1...v12.1.0-dev.2) (2023-08-03)
# [12.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v12.0.0...v12.1.0-dev.1) (2023-08-01)
### Features
* add `MutableMethod.getInstructions` extension function ([fae4029](https://github.com/ReVanced/revanced-patcher/commit/fae4029cfccfad7aa3dd8f7fbef1c63ee26b85b3))
# [12.0.0](https://github.com/ReVanced/revanced-patcher/compare/v11.0.4...v12.0.0) (2023-07-30)
### Bug Fixes
* correct access flags of `PackageMetadata` ([416d691](https://github.com/ReVanced/revanced-patcher/commit/416d69142f50dab49c9ea3f027e9d53e4777f257))
* set resource table via resource decoder ([e0f8e1b](https://github.com/ReVanced/revanced-patcher/commit/e0f8e1b71a295948b610029c89a48f52762396b6))
### Features
* Deprecate `Version` annotation ([c9bbcf2](https://github.com/ReVanced/revanced-patcher/commit/c9bbcf2bf2b0f50ab9100380a3a66c6346ad42ac))
* remove `Path` option ([#202](https://github.com/ReVanced/revanced-patcher/issues/202)) ([69e4a49](https://github.com/ReVanced/revanced-patcher/commit/69e4a490659ebc4fb4bf46148634f4b064ef1713))
### BREAKING CHANGES
* This removes the previously available `Path` option
# [12.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v12.0.0-dev.1...v12.0.0-dev.2) (2023-07-28)
### Features
* Deprecate `Version` annotation ([400442f](https://github.com/ReVanced/revanced-patcher/commit/400442f70ee56cafd4493b2ce64a294db9836509))
# [12.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v11.0.4...v12.0.0-dev.1) (2023-07-26)
### Bug Fixes
* correct access flags of `PackageMetadata` ([416d691](https://github.com/ReVanced/revanced-patcher/commit/416d69142f50dab49c9ea3f027e9d53e4777f257))
* set resource table via resource decoder ([e0f8e1b](https://github.com/ReVanced/revanced-patcher/commit/e0f8e1b71a295948b610029c89a48f52762396b6))
### Features
* remove `Path` option ([#202](https://github.com/ReVanced/revanced-patcher/issues/202)) ([69e4a49](https://github.com/ReVanced/revanced-patcher/commit/69e4a490659ebc4fb4bf46148634f4b064ef1713))
### BREAKING CHANGES
* This removes the previously available `Path` option
## [11.0.4](https://github.com/revanced/revanced-patcher/compare/v11.0.3...v11.0.4) (2023-07-01)

42
arsclib-utils/.gitignore vendored Normal file
View 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

View File

@@ -0,0 +1,18 @@
plugins {
kotlin("jvm")
`maven-publish`
}
group = "app.revanced"
dependencies {
implementation("io.github.reandroid:ARSCLib:1.1.7")
}
java {
withSourcesJar()
}
kotlin {
jvmToolchain(11)
}

View File

@@ -0,0 +1,72 @@
package app.revanced.arsc
/**
* An exception thrown when there is an error with APK resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
sealed class ApkResourceException(message: String, throwable: Throwable? = null) : Exception(message, throwable) {
/**
* An exception when locking resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Locked(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when writing resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Write(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when reading resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Read(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when decoding resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Decode(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception when encoding resources.
*
* @param message The exception message.
* @param throwable The corresponding [Throwable].
*/
class Encode(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
/**
* An exception thrown when a reference could not be resolved.
*
* @param reference The invalid reference.
* @param throwable The corresponding [Throwable].
*/
class InvalidReference(reference: String, throwable: Throwable? = null) :
ApkResourceException("Failed to resolve: $reference", throwable) {
/**
* An exception thrown when a reference could not be resolved.
*
* @param type The type of the reference.
* @param name The name of the reference.
* @param throwable The corresponding [Throwable].
*/
constructor(type: String, name: String, throwable: Throwable? = null) : this("@$type/$name", throwable)
}
/**
* An exception thrown when the Apk file not have a resource table, but was expected to have one.
*/
class MissingResourceTable : ApkResourceException("Apk does not have a resource table.")
}

View File

@@ -0,0 +1,28 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package app.revanced.arsc.archive
import app.revanced.arsc.resource.ResourceContainer
import com.reandroid.apk.ApkModule
import com.reandroid.apk.DexFileInputSource
import com.reandroid.archive.InputSource
import java.io.File
import java.io.Flushable
/**
* A class for reading/writing files in an [ApkModule].
*
* @param module The [ApkModule] to operate on.
*/
class Archive(internal val module: ApkModule) : Flushable {
val mainPackageResources = ResourceContainer(this, module.tableBlock)
fun save(output: File) {
flush()
module.writeApk(output)
}
fun readDexFiles(): MutableList<DexFileInputSource> = module.listDexFiles()
fun write(inputSource: InputSource) = module.apkArchive.add(inputSource) // Overwrites existing files.
fun read(name: String): InputSource? = module.apkArchive.getInputSource(name)
override fun flush() = mainPackageResources.flush()
}

View File

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

View File

@@ -0,0 +1,166 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import com.reandroid.arsc.coder.EncodeResult
import com.reandroid.arsc.coder.ValueDecoder
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.isComplex 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 ApkResourceException.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.getOrCreateString(v))
})
}
}
/**
* A string [Resource].
*
* @param value The string value.
*/
class StringResource(val value: String) : Scalar(ValueType.STRING, 0) {
private fun tableString(resources: ResourceContainer) = resources.getOrCreateString(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))
}

View File

@@ -0,0 +1,167 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.archive.Archive
import com.reandroid.apk.xmlencoder.EncodeUtil
import com.reandroid.arsc.chunk.TableBlock
import com.reandroid.arsc.chunk.xml.ResXmlDocument
import com.reandroid.arsc.value.Entry
import com.reandroid.arsc.value.ResConfig
import java.io.Closeable
import java.io.File
import java.io.Flushable
class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock) : Flushable {
private val packageBlock = tableBlock.pickOne() // Pick the main package block.
internal lateinit var resourceTable: ResourceTable // TODO: Set this.
private val lockedResourceFileNames = mutableSetOf<String>()
private fun lock(resourceFile: ResourceFile) {
if (resourceFile.name in lockedResourceFileNames) {
throw ApkResourceException.Locked("Resource file ${resourceFile.name} is already locked.")
}
lockedResourceFileNames.add(resourceFile.name)
}
private fun unlock(resourceFile: ResourceFile) {
lockedResourceFileNames.remove(resourceFile.name)
}
fun <T : ResourceFile> openResource(name: String): ResourceFileEditor<T> {
val inputSource = archive.read(name)
?: throw ApkResourceException.Read("Resource file $name not found.")
val resourceFile = when {
ResXmlDocument.isResXmlBlock(inputSource.openStream()) -> {
val xmlDocument = archive.module
.loadResXmlDocument(inputSource)
.decodeToXml(resourceTable.entryStore, packageBlock.id)
ResourceFile.XmlResourceFile(name, xmlDocument)
}
else -> {
val bytes = inputSource.openStream().use { it.readAllBytes() }
ResourceFile.BinaryResourceFile(name, bytes)
}
}
try {
@Suppress("UNCHECKED_CAST")
return ResourceFileEditor(resourceFile as T).also {
lockedResourceFileNames.add(name)
}
} catch (e: ClassCastException) {
throw ApkResourceException.Decode("Resource file $name is not ${resourceFile::class}.", e)
}
}
inner class ResourceFileEditor<T : ResourceFile> internal constructor(
private val resourceFile: T,
) : Closeable {
fun use(block: (T) -> Unit) = block(resourceFile)
override fun close() {
lockedResourceFileNames.remove(resourceFile.name)
}
}
override fun flush() {
TODO("Not yet implemented")
}
/**
* Open a resource file, creating it if the file does not exist.
*
* @param path The resource file path.
* @return The corresponding [ResourceFiles],
*/
fun openFile(path: String) = ResourceFiles(createHandle(path), archive)
private fun getPackageBlock() = packageBlock ?: throw ApkResourceException.MissingResourceTable
internal fun getOrCreateString(value: String) =
tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkResourceException.MissingResourceTable
private fun Entry.set(resource: Resource) {
val existingEntryNameReference = specReference
// Sets this.specReference if the entry is not yet initialized.
// Sets this.specReference to 0 if the resource type of the existing entry changes.
ensureComplex(resource.isComplex)
if (existingEntryNameReference != 0) {
// Preserve the entry name by restoring the previous spec block reference (if present).
specReference = existingEntryNameReference
}
resource.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 (_: ApkResourceException.InvalidReference) {
return null
}
val config = ResConfig.parse(qualifiers)
return tableBlock?.resolveReference(resourceId)?.singleOrNull { it.resConfig == config }
}
/**
* Create a [ResourceFiles.Handle] that can be used to open a [ResourceFiles].
* This may involve looking it up in the resource table to find the actual location in the archive.
*
* @param path The path of the resource.
*/
private fun createHandle(path: String): ResourceFiles.Handle {
if (path.startsWith("res/values")) throw ApkResourceException.Decode("Decoding the resource table as a file is not supported")
var onClose = {}
var archivePath = path
if (tableBlock != null && path.startsWith("res/") && path.count { it == '/' } == 2) {
val file = File(path)
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
getEntry(type, name, qualifiers)?.resValue?.valueAsString?.let {
archivePath = it
} ?: run {
// An entry for this specific resource file was not found in the resource table, so we have to register it after we save.
onClose = { setResource(type, name, StringResource(archivePath), qualifiers) }
}
}
return ResourceFiles.Handle(path, archivePath, onClose)
}
fun setResource(type: String, entryName: String, resource: Resource, qualifiers: String? = null) =
getPackageBlock().getOrCreate(qualifiers, type, entryName).also { it.set(resource) }.resourceId
fun setResources(type: String, resources: Map<String, Resource>, configuration: String? = null) {
getPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply {
resources.forEach { (entryName, resource) -> getOrCreateEntry(entryName).set(resource) }
}
}
override fun flush() {
packageBlock?.name = archive
}
}

View File

@@ -0,0 +1,91 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
import app.revanced.arsc.archive.Archive
import com.reandroid.archive.InputSource
import com.reandroid.xml.XMLDocument
import com.reandroid.xml.XMLException
import java.io.*
abstract class ResourceFile(val name: String) {
internal var realName: String? = null
class XmlResourceFile(name: String, val document: XMLDocument) : ResourceFile(name)
class BinaryResourceFile(name: String, var bytes: ByteArray) : ResourceFile(name)
}
class ResourceFiles private constructor(
) : Closeable {
/**
* Instantiate a [ResourceFiles].
*
* @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 ApkResourceException.Decode("Failed to decode XML while reading ${handle.virtualPath}", e)
} catch (e: IOException) {
throw ApkResourceException.Decode("Could not read ${handle.virtualPath}", e)
}
)
companion object {
const val DEFAULT_BUFFER_SIZE = 1024
}
var contents = readResult?.data ?: ByteArray(0)
set(value) {
pendingWrite = true
field = value
}
val exists = readResult != null
override fun toString() = handle.virtualPath
override fun close() {
if (pendingWrite) {
val path = handle.archivePath
if (isXmlResource) archive.writeXml(
path,
try {
XMLDocument.load(inputStream())
} catch (e: XMLException) {
throw ApkResourceException.Encode("Failed to parse XML while writing ${handle.virtualPath}", e)
}
) else archive.writeRaw(path, contents)
}
handle.onClose()
archive.unlock(this)
}
fun inputStream(): InputStream = ByteArrayInputStream(contents)
fun outputStream(bufferSize: Int = DEFAULT_BUFFER_SIZE): OutputStream =
object : ByteArrayOutputStream(bufferSize) {
override fun close() {
this@ResourceFiles.contents = if (buf.size > count) buf.copyOf(count) else buf
super.close()
}
}
/**
* @param virtualPath The resource file path. Example: /res/drawable-hdpi/icon.png.
* @param archivePath The actual file path in the archive. Example: res/4a.png.
* @param onClose An action to perform when the file associated with this handle is closed
*/
internal data class Handle(val virtualPath: String, val archivePath: String, val onClose: () -> Unit)
}

View File

@@ -0,0 +1,100 @@
package app.revanced.arsc.resource
import app.revanced.arsc.ApkResourceException
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.tableBlock!!.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 ApkResourceException.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 ApkResourceException.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.tableBlock
it.tableBlock!!.frameWorks.forEach { fw ->
if (fw is FrameworkTable) {
entryStore.add(fw)
encodeMaterials.addFramework(fw)
}
}
}
}
}

View File

@@ -0,0 +1,56 @@
package app.revanced.arsc.xml
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 to lazily encode an [XMLDocument] after it has been modified.
*
* @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 LazyXMLEncodeSource(
name: String,
val document: XMLDocument,
private val resources: ResourceContainer
) : XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document)) {
private var encoded = false
override fun getResXmlBlock(): ResXmlDocument {
if (encoded) return super.getResXmlBlock()
XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document))
fun XMLElement.registerIds() {
listAttributes().forEach { attr ->
if (!attr.value.startsWith("@+id/")) return@forEach
val name = attr.value.split('/').last()
resources.setResource("id", name, boolean(false))
attr.value = "@id/$name"
}
listChildElements().forEach { it.registerIds() }
}
// Handle all @+id/id_name references in the document.
document.documentElement.registerIds()
encoded = true
// This will call XMLEncodeSource.getResXmlBlock(),
// which will encode the document if it has not already been encoded.
try {
return super.getResXmlBlock()
} catch (e: EncodeException) {
throw EncodeException("Failed to encode $name", e)
}
}
}

View File

@@ -1,74 +1,3 @@
plugins {
kotlin("jvm") version "1.8.20"
`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()
google()
listOf("multidexlib2", "apktool").forEach { repo ->
maven {
url = uri("https://maven.pkg.github.com/revanced/$repo")
credentials {
username = githubUsername
password = githubPassword
}
}
}
}
dependencies {
implementation("xpp3:xpp3:1.1.4c")
implementation("com.android.tools.smali:smali:3.0.3")
implementation("app.revanced:multidexlib2:3.0.3.r2")
implementation("app.revanced:apktool-lib:2.8.2-2")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
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"])
}
}
kotlin("jvm") version "1.8.20" apply false
}

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
org.gradle.parallel=true
org.gradle.caching=true
kotlin.code.style = official
version = 13.0.0-dev.1
version = 11.0.4

View 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"])
}
}
}

View File

@@ -0,0 +1 @@
rootProject.name = "revanced-patcher"

View 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.ResourceFiles
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: ResourceFiles) : 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
}
}

View File

@@ -0,0 +1,222 @@
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
}
// TODO: only emit if the patch is not a closeable.
// If it is a closeable, this should be done when closing the patch.
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)

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ annotation class Description(
* @param version The version of a [Patch].
*/
@Target(AnnotationTarget.CLASS)
@Deprecated("This annotation is deprecated and will be removed in a future release.")
@Deprecated("This annotation is deprecated and will be removed in the future.")
annotation class Version(
val version: String,
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package app.revanced.patcher.apk
import app.revanced.arsc.ApkResourceException
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>) : Sequence<Apk> {
/**
* 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")
}
/**
* The [ResourceTable] of this [ApkBundle].
*/
val resources = ResourceTable(base.resources, map { it.resources })
override fun iterator() = sequence {
yield(base)
splits?.values?.let {
yieldAll(it)
}
}.iterator()
/**
* 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) = map {
var exception: ApkResourceException? = null
try {
it.cleanup(options)
} catch (e: ApkResourceException) {
exception = e
}
SplitApkResult(it, exception)
}
/**
* The result of writing an [Apk] file.
*
* @param apk The corresponding [Apk] file.
* @param exception The optional [ApkResourceException] when an exception occurred.
*/
data class SplitApkResult(val apk: Apk, val exception: ApkResourceException? = null)
}

View File

@@ -1,7 +1,7 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.AccessFlags
import org.jf.dexlib2.AccessFlags
/**
* Create a label for the instruction at given index.

View File

@@ -4,12 +4,12 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patcher.util.smali.toInstruction
import app.revanced.patcher.util.smali.toInstructions
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.Label
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.builder.BuilderInstruction
import org.jf.dexlib2.builder.BuilderOffsetInstruction
import org.jf.dexlib2.builder.Label
import org.jf.dexlib2.builder.MutableMethodImplementation
import org.jf.dexlib2.builder.instruction.*
import org.jf.dexlib2.iface.instruction.Instruction
object InstructionExtensions {
@@ -321,11 +321,6 @@ object InstructionExtensions {
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MutableMethod.getInstruction(index: Int): T = implementation!!.getInstruction<T>(index)
/**
* Get the instructions of a method.
* @return The instructions.
*/
fun MutableMethod.getInstructions(): MutableList<BuilderInstruction> = implementation!!.instructions
}

View File

@@ -4,10 +4,10 @@ import app.revanced.patcher.annotation.Compatibility
import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.RequiresIntegrations
@@ -19,50 +19,50 @@ object PatchExtensions {
/**
* The name of a [Patch].
*/
val Class<out Patch<Context>>.patchName: String
val PatchClass.patchName: String
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
/**
* The version of a [Patch].
*/
@Deprecated("This property is deprecated and will be removed in a future release.")
val Class<out Patch<Context>>.version
@Deprecated("This property is deprecated and will be removed in the future.")
val PatchClass.version
get() = findAnnotationRecursively(Version::class)?.version
/**
* 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
/**
* The description of a [Patch].
*/
val Class<out Patch<Context>>.description
val PatchClass.description
get() = findAnnotationRecursively(Description::class)?.description
/**
* The dependencies of a [Patch].
*/
val Class<out Patch<Context>>.dependencies
val PatchClass.dependencies
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
/**
* The packages a [Patch] is compatible with.
*/
val Class<out Patch<Context>>.compatiblePackages
val PatchClass.compatiblePackages
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
/**
* Weather or not a [Patch] requires integrations.
*/
internal val Class<out Patch<Context>>.requiresIntegrations
internal val PatchClass.requiresIntegrations
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
/**
* The options of a [Patch].
*/
val Class<out Patch<Context>>.options: PatchOptions?
val PatchClass.options: PatchOptions?
get() = kotlin.companionObject?.let { cl ->
if (cl.visibility != KVisibility.PUBLIC) return null
kotlin.companionObjectInstance?.let {

Some files were not shown because too many files have changed in this diff Show More