Use Kotlin Multiplatform, update to new binary compatibility plugin, more or less finish composite api, modularize project for separation of APIs, update Gradle wrapper, add new handy declarative predicate APIs, use new publishing plugin, simplify/modernize build system, standardize gitignore, and optimize build properties for performance

This commit is contained in:
oSumAtrIX
2025-12-14 03:17:33 +01:00
parent 2509997432
commit 2c97de2894
76 changed files with 4120 additions and 4190 deletions

View File

@@ -1,3 +0,0 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled

133
.gitignore vendored
View File

@@ -1,124 +1,19 @@
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
.idea/artifacts
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/*.iml
.idea/modules
*.iml
*.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
.idea/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Gradle template
.kotlin
.gradle
**/build/
xcuserdata
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# Avoid ignoring test resources
!src/test/resources/*
# Dependency directories
local.properties
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
# Gradle props, to avoid sharing the gpr key
gradle.properties

View File

@@ -1,119 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin)
alias(libs.plugins.binary.compatibility.validator)
`maven-publish`
signing
}
group = "app.revanced"
tasks {
processResources {
expand("projectVersion" to project.version)
}
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
}
repositories {
mavenCentral()
google()
maven {
// A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials {
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
// TODO: Convert project to KMP.
compileOnly(libs.android) {
// Exclude, otherwise the org.w3c.dom API breaks.
exclude(group = "xerces", module = "xmlParserAPIs")
}
implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(libs.xpp3)
testImplementation(libs.mockk)
testImplementation(libs.kotlin.test)
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
freeCompilerArgs = listOf("-Xcontext-parameters")
}
}
java {
targetCompatibility = JavaVersion.VERSION_11
withSourcesJar()
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("revanced-patcher-publication") {
from(components["java"])
version = project.version.toString()
pom {
name = "ReVanced Patcher"
description = "Patcher used by ReVanced."
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
url = "https://github.com/revanced/revanced-patcher"
}
}
}
}
}
signing {
useGpgCmd()
sign(publishing.publications["revanced-patcher-publication"])
}
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.vanniktech.mavenPublish) apply false
}

File diff suppressed because it is too large Load Diff

1108
core/api/jvm/core.api Normal file

File diff suppressed because it is too large Load Diff

109
core/build.gradle.kts Normal file
View File

@@ -0,0 +1,109 @@
import com.android.build.api.dsl.androidLibrary
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.android.kotlin.multiplatform.library)
alias(libs.plugins.vanniktech.mavenPublish)
}
group = "app.revanced"
kotlin {
@OptIn(ExperimentalAbiValidation::class)
abiValidation {
enabled = true
}
jvm()
androidLibrary {
namespace = "app.revanced.patcher"
compileSdk = libs.versions.android.compileSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt()
withHostTestBuilder {}.configure {}
withDeviceTestBuilder {
sourceSetTreeName = "test"
}
compilations.configureEach {
compilerOptions.configure {
jvmTarget.set(
JvmTarget.JVM_11
)
}
}
}
sourceSets {
commonMain.dependencies {
implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(libs.xpp3)
}
jvmTest.dependencies {
implementation(libs.mockk)
implementation(libs.kotlin.test)
}
}
compilerOptions {
freeCompilerArgs = listOf("-Xcontext-parameters")
}
}
tasks {
named<ProcessResources>("jvmProcessResources") {
expand("projectVersion" to project.version)
}
named<Test>("jvmTest") {
useJUnitPlatform()
}
}
mavenPublishing {
publishing {
repositories {
maven {
name = "githubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials(PasswordCredentials::class)
}
}
}
signAllPublications()
extensions.getByType<SigningExtension>().useGpgCmd()
coordinates(group.toString(), project.name, version.toString())
pom {
name = "ReVanced Patcher"
description = "Patcher used by ReVanced."
inceptionYear = "2022"
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
url = "https://github.com/revanced/revanced-patcher"
}
}
}

View File

@@ -0,0 +1,32 @@
package app.revanced.patcher.patch
import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Loads patches from DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The DEX files to load the patches from.
*
* @return The loaded patches.
*/
actual fun loadPatches(patchesFiles: Set<File>) = loadPatches(
patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
null,
null, null
)
)

View File

@@ -0,0 +1,7 @@
package collections
actual fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V
) = merge(key, value, remappingFunction)

View File

@@ -0,0 +1,8 @@
package java.io
import java.nio.charset.Charset
internal actual fun File.kmpResolve(child: String) = resolve(child)
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
internal actual fun File.kmpInputStream() = inputStream()
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)

View File

@@ -0,0 +1,598 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package app.revanced.patcher
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
/**
* A fingerprint for a method. A fingerprint is a partial description of a method.
* It is used to uniquely match a method by its characteristics.
*
* An example fingerprint for a public method that takes a single string parameter and returns void:
* ```
* fingerprint {
* accessFlags(AccessFlags.PUBLIC)
* returns("V")
* parameters("Ljava/lang/String;")
* }
* ```
*
* @param accessFlags The exact access flags using values of [AccessFlags].
* @param returnType The return type. Compared using [String.startsWith].
* @param parameters The parameters. Partial matches allowed and follow the same rules as [returnType].
* @param opcodes A pattern of instruction opcodes. `null` can be used as a wildcard.
* @param strings A list of the strings. Compared using [String.contains].
* @param custom A custom condition for this fingerprint.
* @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern.
*/
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List<String>?,
internal val opcodes: List<Opcode?>?,
internal val strings: List<String>?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int,
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
context(_: BytecodePatchContext)
private val matchOrNull: Match?
get() = matchOrNull()
/**
* Match using [BytecodePatchContext.lookupMaps].
*
* Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match.
*
* Fingerprints can be optimized for performance:
* - Slowest: Specify [custom] or [opcodes] and nothing else.
* - Fast: Specify [accessFlags], [returnType].
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(context: BytecodePatchContext)
internal fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
var match = strings?.mapNotNull {
context.classDefs.methodsByString[it]
}?.minByOrNull { it.size }?.let { methodClasses ->
methodClasses.forEach { method ->
val match = matchOrNull(method, context.classDefs[method.definingClass]!!)
if (match != null) return@let match
}
null
}
if (match != null) return match
context.classDefs.forEach { classDef ->
match = matchOrNull(classDef)
if (match != null) return match
}
return null
}
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(_: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): Match? {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
val match = matchOrNull(method, classDef)
if (match != null) return match
}
return null
}
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
) = matchOrNull(method, context.classDefs[method.definingClass]!!)
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
classDef: ClassDef,
): Match? {
if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) {
return null
}
if (accessFlags != null && accessFlags != method.accessFlags) {
return null
}
fun parametersEqual(
parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): Boolean {
if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator()
parameters2.forEach {
if (!it.startsWith(iterator1.next())) return false
}
return true
}
// TODO: parseParameters()
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
return null
}
if (custom != null && !custom.invoke(method, classDef)) {
return null
}
val stringMatches: List<Match.StringMatch>? =
if (strings != null) {
buildList {
val instructions = method.instructionsOrNull ?: return null
val stringsList = strings.toMutableList()
instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(Match.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return null
}
} else {
null
}
val patternMatch = if (opcodes != null) {
val instructions = method.instructionsOrNull ?: return null
fun patternScan(): Match.PatternMatch? {
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
val instructionLength = instructions.count()
val patternLength = opcodes.size
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = opcodes.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// Reaching maximum threshold (0) means,
// the pattern does not match to the current instructions.
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// If the entire pattern has not been scanned yet, continue the scan.
patternIndex++
continue
}
// The entire pattern has been scanned.
return Match.PatternMatch(
index,
index + patternIndex,
)
}
}
return null
}
patternScan() ?: return null
} else {
null
}
_matchOrNull = Match(
context,
classDef,
method,
patternMatch,
stringMatches,
)
return _matchOrNull
}
private val exception get() = PatchException("Failed to match the fingerprint: $this")
/**
* The match for this [Fingerprint].
*
* @throws PatchException If the [Fingerprint] has not been matched.
*/
context(_: BytecodePatchContext)
private val match
get() = matchOrNull ?: throw exception
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw exception
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw exception
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(method, classDef) ?: throw exception
/**
* The class the matching method is a member of.
*/
context(_: BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull?.originalClassDef
/**
* The matching method.
*/
context(_: BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDefOrNull] if mutable access is not required.
*/
context(_: BytecodePatchContext)
val classDefOrNull
get() = matchOrNull?.classDef
/**
* The mutable version of [originalMethodOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(_: BytecodePatchContext)
val methodOrNull
get() = matchOrNull?.method
/**
* The match for the opcode pattern.
*/
context(_: BytecodePatchContext)
val patternMatchOrNull
get() = matchOrNull?.patternMatch
/**
* The matches for the strings.
*/
context(_: BytecodePatchContext)
val stringMatchesOrNull
get() = matchOrNull?.stringMatches
/**
* The class the matching method is a member of.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val originalClassDef
get() = match.originalClassDef
/**
* The matching method.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val originalMethod
get() = match.originalMethod
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val classDef
get() = match.classDef
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethod] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val method
get() = match.method
/**
* The match for the opcode pattern.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val patternMatch
get() = match.patternMatch
/**
* The matches for the strings.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val stringMatches
get() = match.stringMatches
}
/**
* A match of a [Fingerprint].
*
* @param originalClassDef The class the matching method is a member of.
* @param originalMethod The matching method.
* @param patternMatch The match for the opcode pattern.
* @param stringMatches The matches for the strings.
*/
class Match internal constructor(
val context: BytecodePatchContext,
val originalClassDef: ClassDef,
val originalMethod: Method,
val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?,
) {
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a new mutable instance.
* Use [originalClassDef] if mutable access is not required.
*/
val classDef by lazy { context.firstClassDefMutable(originalClassDef.type) }
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*/
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
/**
* A match for an opcode pattern.
* @param startIndex The index of the first opcode of the pattern in the method.
* @param endIndex The index of the last opcode of the pattern in the method.
*/
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
)
/**
* A match for a string.
*
* @param string The string that matched.
* @param index The index of the instruction in the method.
*/
class StringMatch internal constructor(val string: String, val index: Int)
}
/**
* A builder for [Fingerprint].
*
* @property accessFlags The exact access flags using values of [AccessFlags].
* @property returnType The return type compared using [String.startsWith].
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
* @property strings A list of the strings compared each using [String.contains].
* @property customBlock A custom condition for this fingerprint.
* @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning.
*
* @constructor Create a new [FingerprintBuilder].
*/
class FingerprintBuilder internal constructor(
private val fuzzyPatternScanThreshold: Int = 0,
) {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List<String>? = null
private var opcodes: List<Opcode?>? = null
private var strings: List<String>? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(vararg accessFlags: AccessFlags) {
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
}
/**
* Set the return type.
*
* @param returnType The return type compared using [String.startsWith].
*/
fun returns(returnType: String) {
this.returnType = returnType
}
/**
* Set the parameters.
*
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
*/
fun parameters(vararg parameters: String) {
this.parameters = parameters.toList()
}
/**
* Set the opcodes.
*
* @param opcodes An opcode pattern of instructions.
* Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
this.opcodes = opcodes.toList()
}
/**
* Set the opcodes.
*
* @param instructions A list of instructions or opcode names in SMALI format.
* - Wildcard or unknown opcodes can be specified by `null`.
* - Empty lines are ignored.
* - Each instruction must be on a new line.
* - The opcode name is enough, no need to specify the operands.
*
* @throws Exception If an unknown opcode is used.
*/
fun opcodes(instructions: String) {
this.opcodes = instructions.trimIndent().split("\n").filter {
it.isNotBlank()
}.map {
// Remove any operands.
val name = it.split(" ", limit = 1).first().trim()
if (name == "null") return@map null
opcodesByName[name] ?: throw Exception("Unknown opcode: $name")
}
}
/**
* Set the strings.
*
* @param strings A list of strings compared each using [String.contains].
*/
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
/**
* Set a custom condition for this fingerprint.
*
* @param customBlock A custom condition for this fingerprint.
*/
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
this.customBlock = customBlock
}
internal fun build() = Fingerprint(
accessFlags,
returnType,
parameters,
opcodes,
strings,
customBlock,
fuzzyPatternScanThreshold,
)
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
/**
* Create a [Fingerprint].
*
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
* @param block The block to build the [Fingerprint].
*
* @return The created [Fingerprint].
*/
fun fingerprint(
fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit,
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()

View File

@@ -0,0 +1,814 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate", "CONTEXT_RECEIVERS_DEPRECATED")
package app.revanced.patcher
import app.revanced.patcher.extensions.*
import app.revanced.patcher.patch.BytecodePatchContext
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.HiddenApiRestriction
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.*
import com.android.tools.smali.dexlib2.iface.Annotation
import com.android.tools.smali.dexlib2.iface.instruction.*
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22t
import com.android.tools.smali.dexlib2.util.MethodUtil
import com.sun.jdi.StringReference
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
fun Iterable<ClassDef>.anyClassDef(predicate: ClassDef.() -> Boolean) = any(predicate)
fun ClassDef.anyMethod(predicate: Method.() -> Boolean) = methods.any(predicate)
fun ClassDef.anyDirectMethod(predicate: Method.() -> Boolean) = directMethods.any(predicate)
fun ClassDef.anyVirtualMethod(predicate: Method.() -> Boolean) = virtualMethods.any(predicate)
fun ClassDef.anyField(predicate: Field.() -> Boolean) = fields.any(predicate)
fun ClassDef.anyInstanceField(predicate: Field.() -> Boolean) = instanceFields.any(predicate)
fun ClassDef.anyStaticField(predicate: Field.() -> Boolean) = staticFields.any(predicate)
fun ClassDef.anyInterface(predicate: String.() -> Boolean) = interfaces.any(predicate)
fun ClassDef.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate)
fun Method.implementation(predicate: MethodImplementation.() -> Boolean) = implementation?.predicate() ?: false
fun Method.anyParameter(predicate: MethodParameter.() -> Boolean) = parameters.any(predicate)
fun Method.anyParameterType(predicate: CharSequence.() -> Boolean) = parameterTypes.any(predicate)
fun Method.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate)
fun Method.anyHiddenApiRestriction(predicate: HiddenApiRestriction.() -> Boolean) = hiddenApiRestrictions.any(predicate)
fun MethodImplementation.anyInstruction(predicate: Instruction.() -> Boolean) = instructions.any(predicate)
fun MethodImplementation.anyTryBlock(predicate: TryBlock<out ExceptionHandler>.() -> Boolean) = tryBlocks.any(predicate)
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate)
fun Iterable<Instruction>.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate)
fun BytecodePatchContext.firstClassDefOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
with(PredicateContext()) { classDefs.firstOrNull { it.predicate() } }
fun BytecodePatchContext.firstClassDef(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
requireNotNull(firstClassDefOrNull(predicate))
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
firstClassDefOrNull(predicate)?.let { classDefs.getOrReplaceMutable(it) }
fun BytecodePatchContext.firstClassDefMutable(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
requireNotNull(firstClassDefMutableOrNull(predicate))
fun BytecodePatchContext.firstClassDefOrNull(
type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = classDefs[type]?.takeIf {
predicate == null || with(PredicateContext()) { it.predicate() }
}
fun BytecodePatchContext.firstClassDef(
type: String,
predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(firstClassDefOrNull(type, predicate))
fun BytecodePatchContext.firstClassDefMutableOrNull(
type: String,
predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = firstClassDefOrNull(type, predicate)?.let { classDefs.getOrReplaceMutable(it) }
fun BytecodePatchContext.firstClassDefMutable(
type: String,
predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(firstClassDefMutableOrNull(type, predicate))
fun BytecodePatchContext.firstMethodOrNull(predicate: context(PredicateContext) Method.() -> Boolean): Method? {
val methods = classDefs.asSequence().flatMap { it.methods.asSequence() }
with(PredicateContext()) {
return methods.firstOrNull { it.predicate() }
}
}
fun BytecodePatchContext.firstMethod(predicate: context(PredicateContext) Method.() -> Boolean) =
requireNotNull(firstMethodOrNull(predicate))
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: context(PredicateContext) Method.() -> Boolean) =
firstMethodOrNull(predicate)?.let { method ->
firstClassDefMutable(method.definingClass).methods.first {
MethodUtil.methodSignaturesMatch(method, it)
}
}
fun BytecodePatchContext.firstMethodMutable(predicate: context(PredicateContext) Method.() -> Boolean) =
requireNotNull(firstMethodMutableOrNull(predicate))
fun BytecodePatchContext.firstMethodOrNull(
vararg strings: String,
predicate: context(PredicateContext) Method.() -> Boolean = { true },
) = with(PredicateContext()) {
val methodsWithStrings = strings.mapNotNull { classDefs.methodsByString[it] }
if (methodsWithStrings.size != strings.size) return null
methodsWithStrings.minBy { it.size }.firstOrNull { method ->
val containsAllOtherStrings = methodsWithStrings.all { method in it }
containsAllOtherStrings && method.predicate()
}
}
fun BytecodePatchContext.firstMethod(
vararg strings: String,
predicate: context(PredicateContext) Method.() -> Boolean = { true },
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
fun BytecodePatchContext.firstMethodMutableOrNull(
vararg strings: String,
predicate: context(PredicateContext) Method.() -> Boolean = { true },
) = firstMethodOrNull(*strings, predicate = predicate)?.let { method ->
firstClassDefMutable(method.definingClass).methods.first {
MethodUtil.methodSignaturesMatch(
method, it
)
}
}
fun BytecodePatchContext.firstMethodMutable(
vararg strings: String, predicate: context(PredicateContext) Method.() -> Boolean = { true }
) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate))
class CachedReadOnlyProperty<T> internal constructor(
private val block: BytecodePatchContext.(KProperty<*>) -> T
) : ReadOnlyProperty<BytecodePatchContext, T> {
private var value: T? = null
private var cached = false
override fun getValue(thisRef: BytecodePatchContext, property: KProperty<*>): T {
if (!cached) {
value = thisRef.block(property)
cached = true
}
return value!!
}
}
fun gettingFirstClassDefOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDefOrNull(predicate) }
fun gettingFirstClassDef(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDef(predicate) }
fun gettingFirstClassDefMutableOrNull(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDefMutableOrNull(predicate) }
fun gettingFirstClassDefMutable(predicate: context(PredicateContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDefMutable(predicate) }
fun gettingFirstClassDefOrNull(
type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDefOrNull(type, predicate) }
fun gettingFirstClassDef(
type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDef(type, predicate) }
fun gettingFirstClassDefMutableOrNull(
type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDefMutableOrNull(type, predicate) }
fun gettingFirstClassDefMutable(
type: String, predicate: (context(PredicateContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDefMutable(type, predicate) }
fun gettingFirstMethodOrNull(predicate: context(PredicateContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethodOrNull(predicate) }
fun gettingFirstMethod(predicate: context(PredicateContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethod(predicate) }
fun gettingFirstMethodMutableOrNull(predicate: context(PredicateContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethodMutableOrNull(predicate) }
fun gettingFirstMethodMutable(predicate: context(PredicateContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethodMutable(predicate) }
fun gettingFirstMethodOrNull(
vararg strings: String,
predicate: context(PredicateContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethodOrNull(*strings, predicate = predicate) }
fun gettingFirstMethod(
vararg strings: String,
predicate: context(PredicateContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethod(*strings, predicate = predicate) }
fun gettingFirstMethodMutableOrNull(
vararg strings: String,
predicate: context(PredicateContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethodMutableOrNull(*strings, predicate = predicate) }
fun gettingFirstMethodMutable(
vararg strings: String,
predicate: context(PredicateContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethodMutable(*strings, predicate = predicate) }
// region Matcher
// region IndexedMatcher
fun <T> indexedMatcher() = IndexedMatcher<T>()
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
IndexedMatcher<T>().apply(build)
fun <T> Iterable<T>.matchIndexed(build: IndexedMatcher<T>.() -> Unit) =
indexedMatcher(build)(this)
context(_: PredicateContext)
fun <T> Iterable<T>.rememberedMatchIndexed(key: Any, build: IndexedMatcher<T>.() -> Unit) =
indexedMatcher<T>()(key, this, build)
context(matcher: IndexedMatcher<T>)
fun <T> head(
predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean
): T.(Int, Int) -> Boolean = { lastMatchedIndex, currentIndex ->
currentIndex == 0 && predicate(lastMatchedIndex, currentIndex)
}
context(matcher: IndexedMatcher<T>)
fun <T> head(predicate: T.() -> Boolean): T.(Int, Int) -> Boolean =
head { _, _ -> predicate() }
context(matcher: IndexedMatcher<T>)
fun <T> after(
range: IntRange = 1..1,
predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean
): T.(Int, Int) -> Boolean = predicate@{ lastMatchedIndex, currentIndex ->
val distance = currentIndex - lastMatchedIndex
matcher.nextIndex = when {
distance < range.first -> lastMatchedIndex + range.first
distance > range.last -> -1
else -> return@predicate predicate(lastMatchedIndex, currentIndex)
}
false
}
context(matcher: IndexedMatcher<T>)
fun <T> after(range: IntRange = 1..1, predicate: T.() -> Boolean) =
after(range) { _, _ -> predicate() }
context(matcher: IndexedMatcher<T>)
operator fun <T> (T.(Int, Int) -> Boolean).unaryPlus() = matcher.add(this)
class IndexedMatcher<T> : Matcher<T, T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean>() {
private val _indices: MutableList<Int> = mutableListOf()
val indices: List<Int> = _indices
private var lastMatchedIndex = -1
private var currentIndex = -1
var nextIndex: Int? = null
override fun invoke(haystack: Iterable<T>): Boolean {
// Normalize to list
val hay = haystack as? List<T> ?: haystack.toList()
_indices.clear()
this@IndexedMatcher.lastMatchedIndex = -1
currentIndex = -1
data class Frame(
val patternIndex: Int,
val lastMatchedIndex: Int,
val previousFrame: Frame?,
var nextHayIndex: Int,
val matchedIndex: Int
)
val stack = ArrayDeque<Frame>()
stack.add(
Frame(
patternIndex = 0,
lastMatchedIndex = -1,
previousFrame = null,
nextHayIndex = 0,
matchedIndex = -1
)
)
while (stack.isNotEmpty()) {
val frame = stack.last()
if (frame.nextHayIndex >= hay.size || nextIndex == -1) {
stack.removeLast()
nextIndex = null
continue
}
val i = frame.nextHayIndex
currentIndex = i
lastMatchedIndex = frame.lastMatchedIndex
nextIndex = null
if (this[frame.patternIndex](hay[i], lastMatchedIndex, currentIndex)) {
Frame(
patternIndex = frame.patternIndex + 1,
lastMatchedIndex = i,
previousFrame = frame,
nextHayIndex = i + 1,
matchedIndex = i
).also {
if (it.patternIndex == size) {
_indices += buildList(size) {
var f: Frame? = it
while (f != null && f.matchedIndex != -1) {
add(f.matchedIndex)
f = f.previousFrame
}
}.asReversed()
return true
}
}.let(stack::add)
}
frame.nextHayIndex = when (val nextIndex = nextIndex) {
null -> frame.nextHayIndex + 1
-1 -> 0 // Frame will be removed next loop.
else -> nextIndex
}
}
return false
}
}
// endregion
context(_: PredicateContext)
inline operator fun <T, U, reified M : Matcher<T, U>> M.invoke(key: Any, iterable: Iterable<T>, builder: M.() -> Unit) =
remembered(key) { apply(builder) }(iterable)
context(_: PredicateContext)
inline operator fun <T, U, reified M : Matcher<T, U>> M.invoke(
iterable: Iterable<T>,
builder: M.() -> Unit
) = invoke(this@invoke.hashCode(), iterable, builder)
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
var matchIndex = -1
protected set
abstract operator fun invoke(haystack: Iterable<T>): Boolean
}
// endregion Matcher
class PredicateContext internal constructor() : MutableMap<Any, Any> by mutableMapOf()
context(context: PredicateContext)
inline fun <reified V : Any> remembered(key: Any, defaultValue: () -> V) =
context[key] as? V ?: defaultValue().also { context[key] = it }
fun <T> T.declarativePredicate(build: DeclarativePredicateBuilder<T>.() -> Unit) =
DeclarativePredicateBuilder<T>().apply(build).all(this)
context(_: PredicateContext)
fun <T> T.rememberedDeclarativePredicate(key: Any, block: DeclarativePredicateBuilder<T>.() -> Unit): Boolean =
remembered(key) { DeclarativePredicateBuilder<T>().apply(block) }.all(this)
context(_: PredicateContext)
private fun <T> T.rememberedDeclarativePredicate(predicate: context(PredicateContext, T) DeclarativePredicateBuilder<T>.() -> Unit) =
rememberedDeclarativePredicate("declarative predicate build") { predicate() }
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefOrNull { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefMutableOrNull { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(type, predicate))
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(type, predicate))
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodOrNull { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodMutableOrNull { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate))
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate))
fun gettingFirstClassDefByDeclarativePredicateOrNull(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefOrNull(type) { rememberedDeclarativePredicate(predicate) }
fun gettingFirstClassDefByDeclarativePredicate(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(type, predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefMutableOrNull(type) { rememberedDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicate(
type: String,
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(type, predicate) }
fun gettingFirstClassDefByDeclarativePredicateOrNull(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefOrNull { rememberedDeclarativePredicate(predicate) }
fun gettingFirstClassDefByDeclarativePredicate(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefMutableOrNull { rememberedDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicate(
predicate: context(PredicateContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicateOrNull(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodOrNull { rememberedDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicate(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(predicate = predicate) }
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodMutableOrNull { rememberedDeclarativePredicate(predicate) }
fun gettingFirstMethodMutableByDeclarativePredicate(
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(predicate = predicate) }
fun gettingFirstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicate(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(*strings, predicate = predicate) }
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
fun gettingFirstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(*strings, predicate = predicate) }
class DeclarativePredicateBuilder<T> internal constructor() {
private val children = mutableListOf<T.() -> Boolean>()
fun anyOf(block: DeclarativePredicateBuilder<T>.() -> Unit) {
val child = DeclarativePredicateBuilder<T>().apply(block)
children += { child.children.any { it() } }
}
fun predicate(block: T.() -> Boolean) {
children += block
}
fun all(target: T): Boolean = children.all { target.it() }
fun any(target: T): Boolean = children.all { target.it() }
}
fun firstMethodComposite(
vararg strings: String,
builder:
context(PredicateContext, Method, IndexedMatcher<Instruction>, MutableList<String>) DeclarativePredicateBuilder<Method>.() -> Unit
) = with(indexedMatcher<Instruction>()) matcher@{
with(mutableListOf<String>()) strings@{
addAll(strings)
Composition(
indices = this@matcher.indices,
strings = this@strings
) { builder() }
}
}
val m = firstMethodComposite("lookup") {
instructions(
head { string == "str" },
anyOf(),
anyOf(after(1..2, string("also lookup"))),
string("s", String::startsWith),
string(),
literal(),
after(1..4, anyOf()),
noneOf(`is`<Instruction22t>()),
instruction { opcode == Opcode.CONST_STRING },
{ _, _ -> opcode == Opcode.CONST_STRING },
)
instructions {
+head(literal())
if (true)
+after(1..2, string("lookup"))
else
+instruction { opcode == Opcode.CONST_STRING }
add { currentIndex, lastMatchedIndex ->
currentIndex == 2 && opcode == Opcode.CONST_STRING
}
}
instructions {
+anyOf(after(1..2, string("also lookup")), Opcode.IF_EQ())
+head(anyOf(string("s"), "s"(), Opcode.IF_EQ()))
+head(allOf(Opcode.CONST_STRING_JUMBO(), "str"()))
add(instruction { this.opcode == Opcode.CONST_STRING || this.string == "lookup" })
add(instruction { string == "lookup" })
+after(1..2, anyOf(string("s"), Opcode.IF_EQ()))
+string("also lookup")
+"equals"()
+"prefix" { startsWith(it) }
+string { startsWith("prefix") }
+"suffix"(String::endsWith)
+literal(12) { it >= this }
+literal(1232)
+literal { this >= 1123 }
+literal()
+string()
+string { startsWith("s") }
+method()
+reference()
+field()
+`is`<Instruction22t>()
+`is`<WideLiteralInstruction>()
+allOf(`is`<ReferenceInstruction>(), string("test"))
+`is`<ReferenceInstruction> { reference !is StringReference }
+`is`<VariableRegisterInstruction> { registerCount > 2 }
+registers(0, 1, 1, 2)
+noneOf(registers({ size > 3 }), reference { contains("SomeClass") })
+type()
+Opcode.CONST_STRING()
+after(1..2, Opcode.RETURN_VOID())
+reference { startsWith("some") }
+field("s")
+allOf() // Wildcard
+anyOf(noneOf(string(), literal(123)), allOf(Opcode.CONST_STRING(), string("tet")))
+method("abc") { startsWith(it) }
+after(1..2, string("also lookup") { startsWith(it) })
+after(1..2, anyOf(string("a", String::endsWith), Opcode.CONST_4()))
+reference { contains("some") }
+method("name")
+field("name")
+after(1..2, reference("com/example", String::contains))
+after(1..2, reference("lookup()V", String::endsWith))
+after(1..2, reference("Lcom/example;->method()V", String::startsWith))
+after(1..2, reference("Lcom/example;->method()V"))
+after(1..2, reference("Lcom/example;->field:Ljava/lang/String;") { endsWith(it) })
}
}
inline fun <reified T : Instruction> `is`(
crossinline predicate: T.() -> Boolean = { true }
): Instruction.(Int, Int) -> Boolean = { _, _ -> (this as? T)?.predicate() == true }
fun instruction(predicate: Instruction.() -> Boolean): Instruction.(Int, Int) -> Boolean = { _, _ -> predicate() }
fun registers(predicate: IntArray.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ ->
when (this) {
is RegisterRangeInstruction ->
IntArray(registerCount) { startRegister + it }.predicate()
is FiveRegisterInstruction ->
intArrayOf(registerC, registerD, registerE, registerF, registerG).predicate()
is ThreeRegisterInstruction ->
intArrayOf(registerA, registerB, registerC).predicate()
is TwoRegisterInstruction ->
intArrayOf(registerA, registerB).predicate()
is OneRegisterInstruction ->
intArrayOf(registerA).predicate()
else -> false
}
}
fun registers(
vararg registers: Int,
compare: IntArray.(registers: IntArray) -> Boolean = { registers ->
this.size >= registers.size && registers.indices.all { this[it] == registers[it] }
}
) = registers({ compare(registers) })
fun literal(predicate: Long.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean =
{ _, _ -> wideLiteral?.predicate() == true }
fun literal(literal: Long, compare: Long.(Long) -> Boolean = Long::equals) =
literal { compare(literal) }
fun reference(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean =
predicate@{ _, _ -> this.reference?.toString()?.predicate() == true }
fun reference(reference: String, compare: String.(String) -> Boolean = String::equals) =
reference { compare(reference) }
fun field(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ ->
fieldReference?.name?.predicate() == true
}
fun field(name: String, compare: String.(String) -> Boolean = String::equals) =
field { compare(name) }
fun type(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean =
{ _, _ -> type?.predicate() == true }
fun type(type: String, compare: String.(String) -> Boolean = String::equals) =
type { compare(type) }
fun method(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ ->
methodReference?.name?.predicate() == true
}
fun method(name: String, compare: String.(String) -> Boolean = String::equals) =
method { compare(name) }
fun string(compare: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = predicate@{ _, _ ->
this@predicate.string?.compare() == true
}
context(stringsList: MutableList<String>, builder: DeclarativePredicateBuilder<Method>)
fun string(
string: String,
compare: String.(String) -> Boolean = String::equals
): Instruction.(Int, Int) -> Boolean {
if (compare == String::equals) stringsList += string
return string { compare(string) }
}
context(stringsList: MutableList<String>)
operator fun String.invoke(compare: String.(String) -> Boolean = String::equals): Instruction.(Int, Int) -> Boolean {
if (compare == String::equals) stringsList += this
return { _, _ -> string?.compare(this@invoke) == true }
}
operator fun Opcode.invoke(): Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean =
{ _, _ -> opcode == this@invoke }
fun anyOf(
vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean
): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex ->
predicates.any { predicate -> predicate(currentIndex, lastMatchedIndex) }
}
fun allOf(
vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean
): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex ->
predicates.all { predicate -> predicate(currentIndex, lastMatchedIndex) }
}
fun noneOf(
vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean
): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex ->
predicates.none { predicate -> predicate(currentIndex, lastMatchedIndex) }
}
fun DeclarativePredicateBuilder<Method>.accessFlags(vararg flags: AccessFlags) =
predicate { accessFlags(*flags) }
fun DeclarativePredicateBuilder<Method>.returnType(
returnType: String,
compare: String.(String) -> Boolean = String::startsWith
) = predicate { this.returnType.compare(returnType) }
fun DeclarativePredicateBuilder<Method>.name(
name: String,
compare: String.(String) -> Boolean = String::equals
) =
predicate { this.name.compare(name) }
fun DeclarativePredicateBuilder<Method>.definingClass(
definingClass: String,
compare: String.(String) -> Boolean = String::equals
) = predicate { this.definingClass.compare(definingClass) }
fun DeclarativePredicateBuilder<Method>.parameterTypes(vararg parameterTypePrefixes: String) = predicate {
parameterTypes.size == parameterTypePrefixes.size && parameterTypes.zip(parameterTypePrefixes)
.all { (a, b) -> a.startsWith(b) }
}
context(matcher: IndexedMatcher<Instruction>)
fun DeclarativePredicateBuilder<Method>.instructions(
build: IndexedMatcher<Instruction>.() -> Unit
) {
matcher.apply(build)
predicate { implementation { matcher(instructions) } }
}
context(matcher: IndexedMatcher<Instruction>)
fun DeclarativePredicateBuilder<Method>.instructions(
vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean
) = instructions { addAll(predicates) }
fun DeclarativePredicateBuilder<Method>.custom(block: Method.() -> Boolean) {
predicate { block() }
}
class Composition internal constructor(
val indices: List<Int>,
val strings: List<String>,
private val predicate: context(PredicateContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) {
private var _methodOrNull: com.android.tools.smali.dexlib2.mutable.MutableMethod? = null
context(context: BytecodePatchContext)
val methodOrNull: com.android.tools.smali.dexlib2.mutable.MutableMethod?
get() {
if (_methodOrNull == null) {
_methodOrNull = if (strings.isEmpty())
context.firstMethodMutableByDeclarativePredicateOrNull(predicate)
else
context.firstMethodMutableByDeclarativePredicateOrNull(
strings = strings.toTypedArray(),
predicate
)
}
return _methodOrNull
}
context(_: BytecodePatchContext)
val method get() = requireNotNull(methodOrNull)
}

View File

@@ -1,9 +1,11 @@
package app.revanced.patcher
import app.revanced.patcher.patch.*
import com.google.common.annotations.VisibleForTesting
import kotlinx.coroutines.flow.flow
import java.io.Closeable
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* A Patcher.
@@ -11,7 +13,7 @@ import java.util.logging.Logger
* @param config The configuration to use for the patcher.
*/
class Patcher(private val config: PatcherConfig) : Closeable {
private val logger = Logger.getLogger(this::class.java.name)
private val logger = Logger.getLogger(this::class.jvmName)
/**
* The context containing the current state of the patcher.
@@ -69,7 +71,7 @@ class Patcher(private val config: PatcherConfig) : Closeable {
// Recursively execute all dependency patches.
dependencies.forEach { dependency ->
dependency.execute(executedPatches).exception?.let{
dependency.execute(executedPatches).exception?.let {
return PatchResult(
this,
PatchException(
@@ -96,10 +98,9 @@ class Patcher(private val config: PatcherConfig) : Closeable {
context.resourceContext.decodeResources(config.resourceMode)
}
logger.info("Initializing lookup maps")
logger.info("Initializing cache")
// Accessing the lazy lookup maps to initialize them.
context.bytecodeContext.lookupMaps
context.bytecodeContext.classDefs.initializeCache()
logger.info("Executing patches")

View File

@@ -3,7 +3,10 @@ package app.revanced.patcher
import app.revanced.patcher.patch.ResourcePatchContext
import brut.androlib.Config
import java.io.File
import java.io.deleteRecursively
import java.io.resolve
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* The configuration for the patcher.
@@ -38,7 +41,7 @@ class PatcherConfig(
frameworkFileDirectory: String? = null,
) : this(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)
private val logger = Logger.getLogger(PatcherConfig::class.java.name)
private val logger = Logger.getLogger(PatcherConfig::class.jvmName)
/**
* The mode to use for resource decoding and compiling.

View File

@@ -1,16 +1,12 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.dex.mutable.MutableMethod
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.iface.instruction.DualReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.*
import com.android.tools.smali.dexlib2.util.MethodUtil
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser
@@ -20,39 +16,6 @@ import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.StringReader
inline fun <reified T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
((this as? ReferenceInstruction)?.reference as? T)?.predicate() ?: false
inline fun <reified T : Reference> Instruction.reference2(predicate: T.() -> Boolean) =
((this as? DualReferenceInstruction)?.reference2 as? T)?.predicate() ?: false
fun Instruction.methodReference(predicate: MethodReference.() -> Boolean) =
reference(predicate)
fun Instruction.methodReference(methodReference: MethodReference) =
methodReference { MethodUtil.methodSignaturesMatch(methodReference, this) }
fun Instruction.fieldReference(predicate: FieldReference.() -> Boolean) =
reference(predicate)
fun Instruction.fieldReference(fieldName: String) =
fieldReference { name == fieldName }
fun Instruction.type(predicate: String.() -> Boolean) =
reference<TypeReference> { type.predicate() }
fun Instruction.type(type: String) =
type { this == type }
fun Instruction.string(predicate: String.() -> Boolean) =
reference<StringReference> { string.predicate() }
fun Instruction.string(string: String) =
string { this == string }
fun Instruction.opcode(opcode: Opcode) = this.opcode == opcode
fun Instruction.wideLiteral(wideLiteral: Long) = (this as? WideLiteralInstruction)?.wideLiteral == wideLiteral
private inline fun <reified T : Reference> Instruction.reference(): T? =
(this as? ReferenceInstruction)?.reference as? T
@@ -61,24 +24,45 @@ val Instruction.reference: Reference?
get() = reference()
val Instruction.methodReference
get() =
reference<MethodReference>()
get() = reference<MethodReference>()
val Instruction.fieldReference
get() =
reference<FieldReference>()
get() = reference<FieldReference>()
val Instruction.typeReference
get() =
reference<TypeReference>()
get() = reference<TypeReference>()
val Instruction.stringReference
get() =
reference<StringReference>()
get() = reference<StringReference>()
/** TODO: This is technically missing for consistency:
private inline fun <reified T : Reference> Instruction.reference2(): T? =
(this as? DualReferenceInstruction)?.reference2 as? T
val Instruction.reference2: Reference?
get() = reference2()
val Instruction.methodReference2
get() = reference2<MethodReference>()
val Instruction.fieldReference2
get() = reference2<FieldReference>()
val Instruction.typeReference2
get() = reference2<TypeReference>()
val Instruction.stringReference2
get() = reference2<StringReference>()
**/
val Instruction.type
get() = typeReference?.type
val Instruction.string
get() = stringReference?.string
val Instruction.wideLiteral
get() =
(this as? WideLiteralInstruction)?.wideLiteral
get() = (this as? NarrowLiteralInstruction)?.wideLiteral
private const val CLASS_HEADER = ".class LInlineCompiler;\n.super Ljava/lang/Object;\n"
@@ -94,7 +78,7 @@ private val sb by lazy { StringBuilder(512) }
* @param templateMethod The method to compile the instructions against.
* @returns A list of instructions.
*/
fun String.toInstructions(templateMethod: MutableMethod? = null): List<BuilderInstruction> {
fun String.toInstructions(templateMethod: com.android.tools.smali.dexlib2.mutable.MutableMethod? = null): List<BuilderInstruction> {
val parameters = templateMethod?.parameterTypes?.joinToString("") { it } ?: ""
val registers = templateMethod?.implementation?.registerCount ?: 1 // TODO: Should this be 0?
val isStatic = templateMethod?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true

View File

@@ -1,6 +1,6 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.dex.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
@@ -64,7 +64,7 @@ fun MutableMethodImplementation.removeInstructions(count: Int) = removeInstructi
fun MutableMethodImplementation.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = instructions.forEach { replaceInstruction(index, it) }
) = instructions.forEachIndexed { i, instruction -> replaceInstruction(index + i, instruction) }
/**
* Add an instruction to a method at the given index.
@@ -100,7 +100,8 @@ fun MutableMethod.addInstruction(
*
* @param smaliInstructions The instruction to add.
*/
fun MutableMethod.addInstruction(smaliInstructions: String) = implementation!!.addInstruction(smaliInstructions.toInstructions(this).first())
fun MutableMethod.addInstruction(smaliInstructions: String) =
implementation!!.addInstruction(smaliInstructions.toInstructions(this).first())
/**
* Add instructions to a method at the given index.
@@ -118,7 +119,8 @@ fun MutableMethod.addInstructions(
*
* @param instructions The instructions to add.
*/
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) = implementation!!.addInstructions(instructions)
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) =
implementation!!.addInstructions(instructions)
/**
* Add instructions to a method.
@@ -135,7 +137,8 @@ fun MutableMethod.addInstructions(
*
* @param smaliInstructions The instructions to add.
*/
fun MutableMethod.addInstructions(smaliInstructions: String) = implementation!!.addInstructions(smaliInstructions.toInstructions(this))
fun MutableMethod.addInstructions(smaliInstructions: String) =
implementation!!.addInstructions(smaliInstructions.toInstructions(this))
/**
* Add instructions to a method at the given index.
@@ -193,6 +196,7 @@ fun MutableMethod.addInstructionsWithLabels(
i.registerB,
label,
)
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
else -> throw IllegalStateException(

View File

@@ -0,0 +1,287 @@
package app.revanced.patcher.patch
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO
import java.io.Closeable
import java.io.IOException
import java.io.deleteRecursively
import java.io.inputStream
import java.io.resolve
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* A context for patches containing the current state of the bytecode.
*
* @param config The [PatcherConfig] used to create this context.
*/
@Suppress("MemberVisibilityCanBePrivate")
class BytecodePatchContext internal constructor(internal val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable {
private val logger = Logger.getLogger(this::class.jvmName)
inner class ClassDefs private constructor(
dexFile: DexFile,
private val classDefs: MutableSet<ClassDef> = dexFile.classes.toMutableSet()
) :
MutableSet<ClassDef> by classDefs {
private val byType = mutableMapOf<String, ClassDef>()
operator fun get(name: String): ClassDef? = byType[name]
// Better performance according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/commit/9b6d95d4f414a35ed68da37b0ecd8549df1ef63a
private val _methodsByStrings =
LinkedHashMap<String, MutableSet<Method>>(2 * size, 0.5f)
val methodsByString: Map<String, Set<Method>> = _methodsByStrings
// Can have a use-case in the future:
// private val _methodsWithString = methodsByString.values.flatten().toMutableSet()
// val methodsWithString: Set<Method> = _methodsWithString
constructor() : this(
MultiDexIO.readDexFile(
true,
config.apkFile,
BasicDexFileNamer(),
null,
null
)
)
internal val opcodes = dexFile.opcodes
override fun add(element: ClassDef): Boolean {
val added = classDefs.add(element)
if (added) addCache(element)
return added
}
override fun addAll(elements: Collection<ClassDef>): Boolean {
var anyAdded = false
elements.forEach { element ->
val added = classDefs.add(element)
if (added) {
addCache(element)
anyAdded = true
}
}
return anyAdded
}
// TODO: There is one default method "removeIf" in MutableSet, which we cannot override in the common module.
// The method must be overloaded with a NotImplementedException to avoid cache desynchronization.
override fun clear() {
classDefs.clear()
byType.clear()
_methodsByStrings.clear()
}
override fun remove(element: ClassDef): Boolean {
val removed = classDefs.remove(element)
if (removed) removeCache(element)
return removed
}
override fun removeAll(elements: Collection<ClassDef>): Boolean {
var anyRemoved = false
elements.forEach { element ->
val removed = classDefs.remove(element)
if (removed) {
removeCache(element)
anyRemoved = true
}
}
return anyRemoved
}
override fun retainAll(elements: Collection<ClassDef>) =
removeAll(classDefs.asSequence().filter { it !in elements })
private fun addCache(classDef: ClassDef) {
byType[classDef.type] = classDef
classDef.forEachString { method, string ->
_methodsByStrings.getOrPut(string) {
// Maybe adjusting load factor/ initial size can improve performance.
mutableSetOf()
} += method
}
}
private fun removeCache(classDef: ClassDef) {
byType -= classDef.type
classDef.forEachString { method, string ->
if (_methodsByStrings[string]?.also { it -= method }?.isEmpty() == true)
_methodsByStrings -= string
}
}
private fun ClassDef.forEachString(action: (Method, String) -> Unit) {
methods.asSequence().forEach { method ->
method.instructionsOrNull?.asSequence()
?.filterIsInstance<ReferenceInstruction>()
?.map { it.reference }
?.filterIsInstance<StringReference>()
?.map { it.string }
?.forEach { string ->
action(method, string)
}
}
}
/**
* Get a mutable version of the given [classDef], replacing it in the set if necessary.
*
* @param classDef The [ClassDef] to get or replace.
* @return The mutable version of the [classDef].
* @see MutableClassDef
* @see toMutable
*/
fun getOrReplaceMutable(classDef: ClassDef): MutableClassDef {
if (classDef !is MutableClassDef) {
val mutableClassDef = classDef.toMutable()
this -= classDef
this += mutableClassDef
return mutableClassDef
}
return classDef
}
internal fun initializeCache() = classDefs.forEach(::addCache)
internal fun clearCache() {
byType.clear()
_methodsByStrings.clear()
}
}
/**
* The list of classes.
*/
val classDefs = ClassDefs()
/**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
* If no extension is present, the function will return early.
*
* @param bytecodePatch The [BytecodePatch] to merge the extension of.
*/
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
val extensionStream = bytecodePatch.extensionInputStream?.get()
?: return logger.fine("Extension not found")
RawDexIO.readRawDexFile(
extensionStream, 0, null
).classes.forEach { classDef ->
val existingClass = classDefs[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" }
classDefs += classDef
return@forEach
}
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) {
return@let
}
classDefs -= existingClass
classDefs += mergedClass
}
}
extensionStream.close()
}
/**
* Navigate a method.
*
* @param method The method to navigate.
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: MethodReference) = MethodNavigator(method)
/**
* Compile bytecode from the [BytecodePatchContext].
*
* @return The compiled bytecode.
*/
@InternalApi
override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
classDefs.clearCache()
System.gc()
val patchedDexFileResults =
config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
-1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = classDefs.let {
// More performant according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/
// commit/8c26ad08457fb1565ea5794b7930da42a1c81cf1
// #diff-be698366d9868784ecf7da3fd4ac9d2b335b0bb637f9f618fbe067dbd6830b8fR197
// TODO: Benchmark, if actually faster.
HashSet<ClassDef>(it.size * 3 / 2).apply { addAll(it) }
}
override fun getOpcodes() = classDefs.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
}.listFiles { it.isFile }!!.map {
PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
return patchedDexFileResults
}
override fun close() {
try {
classDefs.clear()
} catch (e: IOException) {
logger.warning("Failed to clear BytecodePatchContext: ${e.message}")
}
}
}

View File

@@ -1,7 +1,10 @@
@file:Suppress("unused")
package app.revanced.patcher.patch
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.jvm.jvmName
import kotlin.reflect.typeOf
/**
@@ -162,8 +165,8 @@ class Options internal constructor(
(option as Option<T>).value = value
} catch (e: ClassCastException) {
throw OptionException.InvalidValueTypeException(
value?.let { it::class.java.name } ?: "null",
option.value?.let { it::class.java.name } ?: "null",
value?.let { it::class.jvmName } ?: "null",
option.value?.let { it::class.jvmName } ?: "null",
)
}
}

View File

@@ -4,17 +4,12 @@ package app.revanced.patcher.patch
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext
import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.io.InputStream
import java.lang.reflect.Member
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.net.URLClassLoader
import java.util.function.Supplier
import java.util.jar.JarFile
import kotlin.properties.ReadOnlyProperty
typealias PackageName = String
@@ -33,7 +28,7 @@ sealed interface PatchContext<T> : Supplier<T>
*
* @param C The [PatchContext] to execute and finalize the patch with.
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param dependencies Other patches this patch depends on.
@@ -127,7 +122,7 @@ internal fun Iterable<Patch<*>>.forEachRecursively(
* A bytecode patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
@@ -149,7 +144,7 @@ class BytecodePatch internal constructor(
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
val extensionInputStream: Supplier<InputStream>?,
internal val extensionInputStream: Supplier<InputStream>?,
executeBlock: (BytecodePatchContext) -> Unit,
finalizeBlock: ((BytecodePatchContext) -> Unit)?,
) : Patch<BytecodePatchContext>(
@@ -176,7 +171,7 @@ class BytecodePatch internal constructor(
* A raw resource patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
@@ -219,7 +214,7 @@ class RawResourcePatch internal constructor(
* A resource patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
@@ -263,7 +258,7 @@ class ResourcePatch internal constructor(
*
* @param C The [PatchContext] to execute and finalize the patch with.
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property compatiblePackages The packages the patch is compatible with.
@@ -380,7 +375,7 @@ private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply
* A [BytecodePatchBuilder] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property extensionInputStream Getter for the extension input stream of the patch.
@@ -393,23 +388,20 @@ class BytecodePatchBuilder internal constructor(
description: String?,
use: Boolean,
) : PatchBuilder<BytecodePatchContext>(name, description, use) {
// Must be internal for the inlined function "extendWith".
@PublishedApi
internal var extensionInputStream: Supplier<InputStream>? = null
// Inlining is necessary to get the class loader that loaded the patch
// to load the extension from the resources.
/**
* Set the extension of the patch.
*
* @param extension The name of the extension resource.
*/
@Suppress("NOTHING_TO_INLINE")
inline fun extendWith(extension: String) = apply {
val classLoader = object {}.javaClass.classLoader
fun extendWith(extension: String) = apply {
// Should be the classloader which loaded the patch class.
val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!!
extensionInputStream = Supplier {
classLoader.getResourceAsStream(extension) ?: throw PatchException("Extension \"$extension\" not found")
classLoader.getResourceAsStream(extension)
?: throw PatchException("Extension \"$extension\" not found")
}
}
@@ -430,7 +422,7 @@ class BytecodePatchBuilder internal constructor(
* Create a new [BytecodePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
@@ -463,7 +455,7 @@ fun gettingBytecodePatch(
* A [RawResourcePatch] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
*
@@ -490,7 +482,7 @@ class RawResourcePatchBuilder internal constructor(
* Create a new [RawResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
@@ -522,7 +514,7 @@ fun gettingRawResourcePatch(
* A [ResourcePatch] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
*
@@ -549,7 +541,7 @@ class ResourcePatchBuilder internal constructor(
* Create a new [ResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* If null, the patch is named "Patch" and will not be loaded by [loadPatches].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
@@ -598,142 +590,45 @@ class PatchException(errorMessage: String?, cause: Throwable?) : Exception(error
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
/**
* A loader for patches.
* A collection of patches loaded from patches files.
*
* Loads unnamed patches from JAR or DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
*
* @param byPatchesFile The patches associated by the patches file they were loaded from.
* @property patchesByFile The patches mapped by their patches file.
*/
sealed class PatchLoader private constructor(
val byPatchesFile: Map<File, Set<Patch<*>>>,
) : Set<Patch<*>> by byPatchesFile.values.flatten().toSet() {
/**
* @param patchesFiles A set of JAR or DEX files to load the patches from.
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
* @param classLoader The [ClassLoader] to use for loading the classes.
*/
private constructor(
patchesFiles: Set<File>,
getBinaryClassNames: (patchesFile: File) -> List<String>,
classLoader: ClassLoader,
) : this(classLoader.loadPatches(patchesFiles.associateWith { getBinaryClassNames(it).toSet() }))
class Patches internal constructor(val patchesByFile: Map<File, Set<Patch<*>>>) : Set<Patch<*>>
by patchesByFile.values.flatten().toSet()
/**
* A [PatchLoader] for JAR files.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @constructor Create a new [PatchLoader] for JAR files.
*/
class Jar(patchesFiles: Set<File>) :
PatchLoader(
patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
)
internal fun loadPatches(
patchesFiles: Set<File>,
getBinaryClassNames: (patchesFile: File) -> List<String>,
classLoader: ClassLoader,
): Patches {
fun Member.isUsable(): Boolean {
if (this is Method && parameterCount != 0) return false
/**
* A [PatchLoader] for [Dex] files.
*
* @param patchesFiles The DEX files to load the patches from.
* @param optimizedDexDirectory The directory to store optimized DEX files in.
* This parameter is deprecated and has no effect since API level 26.
*
* @constructor Create a new [PatchLoader] for [Dex] files.
*/
class Dex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) :
PatchLoader(
patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null,
this::class.java.classLoader,
),
)
// Companion object required for unit tests.
private companion object {
val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Public static fields that are patches.
*/
private val Class<*>.patchFields
get() = fields.filter { field ->
field.type.isPatch && field.canAccess()
}.map { field ->
field.get(null) as Patch<*>
}
/**
* Public static and non-parametrized methods that return patches.
*/
private val Class<*>.patchMethods
get() = methods.filter { method ->
method.returnType.isPatch && method.parameterCount == 0 && method.canAccess()
}.map { method ->
method.invoke(null) as Patch<*>
}
/**
* Loads unnamed patches declared as public static fields
* or returned by public static and non-parametrized methods.
*
* @param binaryClassNamesByPatchesFile The binary class name of the classes to load the patches from
* associated by the patches file.
*
* @return The loaded patches associated by the patches file.
*/
private fun ClassLoader.loadPatches(binaryClassNamesByPatchesFile: Map<File, Set<String>>) =
binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) ->
binaryClassNames.asSequence().map {
loadClass(it)
}.flatMap {
it.patchFields + it.patchMethods
}.filter {
it.name != null
}.toSet()
}
private fun Member.canAccess(): Boolean {
if (this is Method && parameterCount != 0) return false
return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)
}
return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)
}
fun Class<*>.getPatchFields() = fields.filter { field ->
field.type.isPatch && field.isUsable()
}.map { field ->
field.get(null) as Patch<*>
}
fun Class<*>.getPatchMethods() = methods.filter { method ->
method.returnType.isPatch && method.parameterCount == 0 && method.isUsable()
}.map { method ->
method.invoke(null) as Patch<*>
}
return Patches(patchesFiles.associateWith { file ->
getBinaryClassNames(file).map {
classLoader.loadClass(it)
}.flatMap { clazz ->
clazz.getPatchMethods() + clazz.getPatchFields()
}.filter { it.name != null }.toSet()
})
}
/**
* Loads patches from JAR files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromJar(patchesFiles: Set<File>) =
PatchLoader.Jar(patchesFiles)
expect fun loadPatches(patchesFiles: Set<File>): Patches
/**
* Loads patches from DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The DEX files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromDex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) =
PatchLoader.Dex(patchesFiles, optimizedDexDirectory)
internal expect val Class<*>.isPatch: Boolean

View File

@@ -16,8 +16,10 @@ import brut.androlib.res.xml.ResXmlUtils
import brut.directory.ExtFile
import java.io.InputStream
import java.io.OutputStream
import java.io.resolve
import java.nio.file.Files
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* A context for patches containing the current state of resources.
@@ -29,7 +31,7 @@ class ResourcePatchContext internal constructor(
private val packageMetadata: PackageMetadata,
private val config: PatcherConfig,
) : PatchContext<PatcherResult.PatchedResources?> {
private val logger = Logger.getLogger(ResourcePatchContext::class.java.name)
private val logger = Logger.getLogger(ResourcePatchContext::class.jvmName)
/**
* Read a document from an [InputStream].

View File

@@ -1,6 +1,12 @@
package app.revanced.patcher.util
import app.revanced.patcher.dex.mutable.MutableClassDef
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableField
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.firstClassDefOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
import app.revanced.patcher.util.ClassMerger.Utils.filterAny
@@ -8,23 +14,19 @@ import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
import app.revanced.patcher.util.ClassMerger.Utils.isPublic
import app.revanced.patcher.util.ClassMerger.Utils.toPublic
import app.revanced.patcher.util.ClassMerger.Utils.traverseClassHierarchy
import app.revanced.patcher.dex.mutable.MutableClassDef.Companion.toMutable
import app.revanced.patcher.dex.mutable.MutableField
import app.revanced.patcher.dex.mutable.MutableField.Companion.toMutable
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.util.MethodUtil
import java.util.logging.Logger
import kotlin.reflect.KFunction2
import kotlin.reflect.jvm.jvmName
/**
* Experimental class to merge a [ClassDef] with another.
* Note: This will not consider method implementations or if the class is missing a superclass or interfaces.
*/
internal object ClassMerger {
private val logger = Logger.getLogger(ClassMerger::class.java.name)
private val logger = Logger.getLogger(ClassMerger::class.jvmName)
/**
* Merge a class with [otherClass].
@@ -181,12 +183,13 @@ internal object ClassMerger {
callback(targetClass)
targetClass.superclass ?: return
classDefs.find { targetClass.superclass == it.type }?.mutable()?.let {
traverseClassHierarchy(it, callback)
firstClassDefOrNull { type == targetClass.superclass }?.let { classDef ->
traverseClassHierarchy(classDefs.getOrReplaceMutable(classDef), callback)
}
}
fun ClassDef.asMutableClass() = if (this is MutableClassDef) this else this.toMutable()
fun ClassDef.asMutableClass() = this as? MutableClassDef ?: this.toMutable()
/**
* Check if the [AccessFlags.PUBLIC] flag is set.

View File

@@ -1,14 +1,19 @@
package app.revanced.patcher.util
import collections.merge
import com.google.common.base.Charsets
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.bufferedWriter
import java.io.inputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.use
class Document internal constructor(
inputStream: InputStream,
@@ -29,7 +34,7 @@ class Document internal constructor(
if (readerCount[it]!! > 1) {
throw IllegalStateException(
"Two or more instances are currently reading $it." +
"To be able to close this instance, no other instances may be reading $it at the same time.",
"To be able to close this instance, no other instances may be reading $it at the same time.",
)
} else {
readerCount.remove(it)
@@ -40,9 +45,9 @@ class Document internal constructor(
if (isAndroid) {
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-16")
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
it.bufferedWriter(charset = Charsets.UTF_8).use { writer ->
transformer.transform(DOMSource(this), StreamResult(writer))
}
val writer = it.bufferedWriter(charset = Charsets.UTF_8)
transformer.transform(DOMSource(this), StreamResult(writer))
writer.close()
} else {
transformer.transform(DOMSource(this), StreamResult(it))
}

View File

@@ -2,8 +2,10 @@
package app.revanced.patcher.util
import app.revanced.patcher.dex.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.firstClassDef
import app.revanced.patcher.firstClassDefMutable
import app.revanced.patcher.patch.BytecodePatchContext
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
@@ -82,10 +84,9 @@ class MethodNavigator internal constructor(
* @return The last navigated method mutably.
*/
context(context: BytecodePatchContext)
fun stop() = with(context) {
classDefs.find(matchesCurrentMethodReferenceDefiningClass)!!.mutable().firstMethodBySignature
as MutableMethod
}
fun stop() = context.firstClassDefMutable(lastNavigatedMethodReference.definingClass)
.firstMethodBySignature as MutableMethod
/**
* Get the last navigated method mutably.
@@ -101,14 +102,7 @@ class MethodNavigator internal constructor(
* @return The last navigated method immutably.
*/
context(context: BytecodePatchContext)
fun original(): Method = context.classDefs.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
/**
* Predicate to match the class defining the current method reference.
*/
private val matchesCurrentMethodReferenceDefiningClass = { classDef: ClassDef ->
classDef.type == lastNavigatedMethodReference.definingClass
}
fun original(): Method = context.firstClassDef(lastNavigatedMethodReference.definingClass).firstMethodBySignature
/**
* Find the first [lastNavigatedMethodReference] in the class.

View File

@@ -0,0 +1,13 @@
package collections
internal expect fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V,
)
internal fun <K, V> MutableMap<K, V>.merge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V,
) = kmpMerge(key, value, remappingFunction)

View File

@@ -1,9 +1,7 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import app.revanced.patcher.dex.mutable.MutableAnnotationElement.Companion.toMutable
import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
import com.android.tools.smali.dexlib2.iface.AnnotationElement
import com.android.tools.smali.dexlib2.iface.value.AnnotationEncodedValue
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
BaseAnnotationEncodedValue(),
@@ -14,15 +12,15 @@ class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedVal
annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet()
}
override fun getType(): String = this.type
fun setType(type: String) {
this.type = type
}
override fun getElements(): MutableSet<out AnnotationElement> = _elements
override fun getType() = this.type
override fun getElements() = _elements
companion object {
fun AnnotationEncodedValue.toMutable(): MutableAnnotationEncodedValue = MutableAnnotationEncodedValue(this)
fun AnnotationEncodedValue.toMutable() = MutableAnnotationEncodedValue(this)
}
}

View File

@@ -0,0 +1,16 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseArrayEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) : BaseArrayEncodedValue(), MutableEncodedValue {
private val _value by lazy {
arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
}
override fun getValue() = _value
companion object {
fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue = MutableArrayEncodedValue(this)
}
}

View File

@@ -1,19 +1,18 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue
import com.android.tools.smali.dexlib2.iface.value.BooleanEncodedValue
class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) :
BaseBooleanEncodedValue(),
MutableEncodedValue {
private var value = booleanEncodedValue.value
override fun getValue(): Boolean = this.value
fun setValue(value: Boolean) {
this.value = value
}
override fun getValue(): Boolean = this.value
companion object {
fun BooleanEncodedValue.toMutable(): MutableBooleanEncodedValue = MutableBooleanEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseByteEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) :
BaseByteEncodedValue(),
MutableEncodedValue {
class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) : BaseByteEncodedValue(), MutableEncodedValue {
private var value = byteEncodedValue.value
override fun getValue(): Byte = this.value
fun setValue(value: Byte) {
this.value = value
}
override fun getValue(): Byte = this.value
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseCharEncodedValue
import com.android.tools.smali.dexlib2.iface.value.CharEncodedValue
class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) :
BaseCharEncodedValue(),
MutableEncodedValue {
class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) : BaseCharEncodedValue(), MutableEncodedValue {
private var value = charEncodedValue.value
override fun getValue(): Char = this.value
fun setValue(value: Char) {
this.value = value
}
override fun getValue(): Char = this.value
companion object {
fun CharEncodedValue.toMutable(): MutableCharEncodedValue = MutableCharEncodedValue(this)
}

View File

@@ -1,19 +1,18 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue
import com.android.tools.smali.dexlib2.iface.value.DoubleEncodedValue
class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) :
BaseDoubleEncodedValue(),
MutableEncodedValue {
private var value = doubleEncodedValue.value
override fun getValue(): Double = this.value
fun setValue(value: Double) {
this.value = value
}
override fun getValue(): Double = this.value
companion object {
fun DoubleEncodedValue.toMutable(): MutableDoubleEncodedValue = MutableDoubleEncodedValue(this)
}

View File

@@ -1,7 +1,6 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.ValueType
import com.android.tools.smali.dexlib2.iface.value.*
interface MutableEncodedValue : EncodedValue {
companion object {

View File

@@ -1,20 +1,17 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseEnumEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.value.EnumEncodedValue
class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) :
BaseEnumEncodedValue(),
MutableEncodedValue {
class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) : BaseEnumEncodedValue(), MutableEncodedValue {
private var value = enumEncodedValue.value
override fun getValue(): FieldReference = this.value
fun setValue(value: FieldReference) {
this.value = value
}
override fun getValue(): FieldReference = this.value
companion object {
fun EnumEncodedValue.toMutable(): MutableEnumEncodedValue = MutableEnumEncodedValue(this)
}

View File

@@ -1,23 +1,20 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.ValueType
import com.android.tools.smali.dexlib2.base.value.BaseFieldEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.value.FieldEncodedValue
class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) :
BaseFieldEncodedValue(),
MutableEncodedValue {
class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) : BaseFieldEncodedValue(), MutableEncodedValue {
private var value = fieldEncodedValue.value
override fun getValueType(): Int = ValueType.FIELD
override fun getValue(): FieldReference = this.value
fun setValue(value: FieldReference) {
this.value = value
}
override fun getValueType(): Int = ValueType.FIELD
override fun getValue(): FieldReference = this.value
companion object {
fun FieldEncodedValue.toMutable(): MutableFieldEncodedValue = MutableFieldEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseFloatEncodedValue
import com.android.tools.smali.dexlib2.iface.value.FloatEncodedValue
class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) :
BaseFloatEncodedValue(),
MutableEncodedValue {
class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) : BaseFloatEncodedValue(), MutableEncodedValue {
private var value = floatEncodedValue.value
override fun getValue(): Float = this.value
fun setValue(value: Float) {
this.value = value
}
override fun getValue(): Float = this.value
companion object {
fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue = MutableFloatEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseIntEncodedValue
import com.android.tools.smali.dexlib2.iface.value.IntEncodedValue
class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) :
BaseIntEncodedValue(),
MutableEncodedValue {
class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) : BaseIntEncodedValue(), MutableEncodedValue {
private var value = intEncodedValue.value
override fun getValue(): Int = this.value
fun setValue(value: Int) {
this.value = value
}
override fun getValue(): Int = this.value
companion object {
fun IntEncodedValue.toMutable(): MutableIntEncodedValue = MutableIntEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseLongEncodedValue
import com.android.tools.smali.dexlib2.iface.value.LongEncodedValue
class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) :
BaseLongEncodedValue(),
MutableEncodedValue {
class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) : BaseLongEncodedValue(), MutableEncodedValue {
private var value = longEncodedValue.value
override fun getValue(): Long = this.value
fun setValue(value: Long) {
this.value = value
}
override fun getValue(): Long = this.value
companion object {
fun LongEncodedValue.toMutable(): MutableLongEncodedValue = MutableLongEncodedValue(this)
}

View File

@@ -1,20 +1,19 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.value.MethodEncodedValue
class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) :
BaseMethodEncodedValue(),
MutableEncodedValue {
private var value = methodEncodedValue.value
override fun getValue(): MethodReference = this.value
fun setValue(value: MethodReference) {
this.value = value
}
override fun getValue(): MethodReference = this.value
companion object {
fun MethodEncodedValue.toMutable(): MutableMethodEncodedValue = MutableMethodEncodedValue(this)
}

View File

@@ -1,20 +1,19 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodHandleEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodHandleReference
import com.android.tools.smali.dexlib2.iface.value.MethodHandleEncodedValue
class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEncodedValue) :
BaseMethodHandleEncodedValue(),
MutableEncodedValue {
private var value = methodHandleEncodedValue.value
override fun getValue(): MethodHandleReference = this.value
fun setValue(value: MethodHandleReference) {
this.value = value
}
override fun getValue(): MethodHandleReference = this.value
companion object {
fun MethodHandleEncodedValue.toMutable(): MutableMethodHandleEncodedValue = MutableMethodHandleEncodedValue(this)
}

View File

@@ -1,20 +1,17 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodTypeEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference
import com.android.tools.smali.dexlib2.iface.value.MethodTypeEncodedValue
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) :
BaseMethodTypeEncodedValue(),
MutableEncodedValue {
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(), MutableEncodedValue {
private var value = methodTypeEncodedValue.value
override fun getValue(): MethodProtoReference = this.value
fun setValue(value: MethodProtoReference) {
this.value = value
}
override fun getValue(): MethodProtoReference = this.value
companion object {
fun MethodTypeEncodedValue.toMutable(): MutableMethodTypeEncodedValue = MutableMethodTypeEncodedValue(this)
}

View File

@@ -0,0 +1,9 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseNullEncodedValue
class MutableNullEncodedValue : BaseNullEncodedValue(), MutableEncodedValue {
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}
}

View File

@@ -1,19 +1,18 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseShortEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ShortEncodedValue
class MutableShortEncodedValue(shortEncodedValue: ShortEncodedValue) :
BaseShortEncodedValue(),
MutableEncodedValue {
private var value = shortEncodedValue.value
override fun getValue(): Short = this.value
fun setValue(value: Short) {
this.value = value
}
override fun getValue(): Short = this.value
companion object {
fun ShortEncodedValue.toMutable(): MutableShortEncodedValue = MutableShortEncodedValue(this)
}

View File

@@ -1,20 +1,18 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseStringEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue
class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) :
BaseStringEncodedValue(),
MutableEncodedValue {
private var value = stringEncodedValue.value
override fun getValue(): String = this.value
fun setValue(value: String) {
this.value = value
}
override fun getValue(): String = this.value
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}

View File

@@ -1,19 +1,16 @@
package app.revanced.patcher.dex.mutable.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseTypeEncodedValue
import com.android.tools.smali.dexlib2.iface.value.TypeEncodedValue
class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) :
BaseTypeEncodedValue(),
MutableEncodedValue {
class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) : BaseTypeEncodedValue(), MutableEncodedValue {
private var value = typeEncodedValue.value
override fun getValue(): String = this.value
fun setValue(value: String) {
this.value = value
}
override fun getValue() = this.value
companion object {
fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue = MutableTypeEncodedValue(this)
}

View File

@@ -1,21 +1,21 @@
package app.revanced.patcher.dex.mutable
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.dex.mutable.MutableAnnotationElement.Companion.toMutable
import com.android.tools.smali.dexlib2.base.BaseAnnotation
import com.android.tools.smali.dexlib2.iface.Annotation
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
class MutableAnnotation(annotation: Annotation) : BaseAnnotation() {
private val visibility = annotation.visibility
private val type = annotation.type
private val _elements by lazy { annotation.elements.map { element -> element.toMutable() }.toMutableSet() }
override fun getType(): String = type
override fun getType() = type
override fun getElements(): MutableSet<MutableAnnotationElement> = _elements
override fun getElements() = _elements
override fun getVisibility(): Int = visibility
override fun getVisibility() = visibility
companion object {
fun Annotation.toMutable(): MutableAnnotation = MutableAnnotation(this)
fun Annotation.toMutable() = MutableAnnotation(this)
}
}

View File

@@ -1,10 +1,9 @@
package app.revanced.patcher.dex.mutable
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
import com.android.tools.smali.dexlib2.iface.AnnotationElement
import com.android.tools.smali.dexlib2.iface.value.EncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() {
private var name = annotationElement.name
@@ -18,11 +17,11 @@ class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnot
this.value = value
}
override fun getName(): String = name
override fun getName() = name
override fun getValue(): EncodedValue = value
override fun getValue() = value
companion object {
fun AnnotationElement.toMutable(): MutableAnnotationElement = MutableAnnotationElement(this)
fun AnnotationElement.toMutable() = MutableAnnotationElement(this)
}
}

View File

@@ -1,16 +1,14 @@
package app.revanced.patcher.dex.mutable
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.dex.mutable.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.dex.mutable.MutableField.Companion.toMutable
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.util.FieldUtil
import com.android.tools.smali.dexlib2.util.MethodUtil
class MutableClassDef(classDef: ClassDef) :
BaseTypeReference(),
ClassDef {
class MutableClassDef(classDef: ClassDef) : BaseTypeReference(), ClassDef {
// Class
private var type = classDef.type
private var sourceFile = classDef.sourceFile
@@ -24,8 +22,8 @@ class MutableClassDef(classDef: ClassDef) :
// Methods
private val _methods by lazy { classDef.methods.map { method -> method.toMutable() }.toMutableSet() }
private val _directMethods by lazy { _methods.filter { method -> MethodUtil.isDirect(method) }.toMutableSet() }
private val _virtualMethods by lazy { _methods.filter { method -> !MethodUtil.isDirect(method) }.toMutableSet() }
private val _directMethods by lazy { methods.filter { method -> MethodUtil.isDirect(method) }.toMutableSet() }
private val _virtualMethods by lazy { methods.filter { method -> !MethodUtil.isDirect(method) }.toMutableSet() }
// Fields
private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
@@ -48,29 +46,29 @@ class MutableClassDef(classDef: ClassDef) :
this.superclass = superclass
}
override fun getType(): String = type
override fun getType() = type
override fun getAccessFlags(): Int = accessFlags
override fun getAccessFlags() = accessFlags
override fun getSourceFile(): String? = sourceFile
override fun getSourceFile() = sourceFile
override fun getSuperclass(): String? = superclass
override fun getSuperclass() = superclass
override fun getInterfaces(): MutableList<String> = _interfaces
override fun getInterfaces() = _interfaces
override fun getAnnotations(): MutableSet<MutableAnnotation> = _annotations
override fun getAnnotations() = _annotations
override fun getStaticFields(): MutableSet<MutableField> = _staticFields
override fun getStaticFields() = _staticFields
override fun getInstanceFields(): MutableSet<MutableField> = _instanceFields
override fun getInstanceFields() = _instanceFields
override fun getFields(): MutableSet<MutableField> = _fields
override fun getFields() = _fields
override fun getDirectMethods(): MutableSet<MutableMethod> = _directMethods
override fun getDirectMethods() = _directMethods
override fun getVirtualMethods(): MutableSet<MutableMethod> = _virtualMethods
override fun getVirtualMethods() = _virtualMethods
override fun getMethods(): MutableSet<MutableMethod> = _methods
override fun getMethods() = _methods
companion object {
fun ClassDef.toMutable(): MutableClassDef = MutableClassDef(this)

View File

@@ -1,15 +1,12 @@
package app.revanced.patcher.dex.mutable
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.dex.mutable.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.HiddenApiRestriction
import com.android.tools.smali.dexlib2.base.reference.BaseFieldReference
import com.android.tools.smali.dexlib2.iface.Field
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
class MutableField(field: Field) :
BaseFieldReference(),
Field {
class MutableField(field: Field) : BaseFieldReference(), Field {
private var definingClass = field.definingClass
private var name = field.name
private var type = field.type
@@ -39,21 +36,21 @@ class MutableField(field: Field) :
this.initialValue = initialValue
}
override fun getDefiningClass(): String = this.definingClass
override fun getDefiningClass() = this.definingClass
override fun getName(): String = this.name
override fun getName() = this.name
override fun getType(): String = this.type
override fun getType() = this.type
override fun getAnnotations(): MutableSet<MutableAnnotation> = this._annotations
override fun getAnnotations() = this._annotations
override fun getAccessFlags(): Int = this.accessFlags
override fun getAccessFlags() = this.accessFlags
override fun getHiddenApiRestrictions(): MutableSet<HiddenApiRestriction> = this._hiddenApiRestrictions
override fun getHiddenApiRestrictions() = this._hiddenApiRestrictions
override fun getInitialValue(): MutableEncodedValue? = this.initialValue
override fun getInitialValue() = this.initialValue
companion object {
fun Field.toMutable(): MutableField = MutableField(this)
fun Field.toMutable() = MutableField(this)
}
}

View File

@@ -1,21 +1,18 @@
package app.revanced.patcher.dex.mutable
package com.android.tools.smali.dexlib2.mutable
import app.revanced.patcher.dex.mutable.MutableAnnotation.Companion.toMutable
import app.revanced.patcher.dex.mutable.MutableMethodParameter.Companion.toMutable
import com.android.tools.smali.dexlib2.HiddenApiRestriction
import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethodParameter.Companion.toMutable
class MutableMethod(method: Method) :
BaseMethodReference(),
Method {
class MutableMethod(method: Method) : BaseMethodReference(), Method {
private var definingClass = method.definingClass
private var name = method.name
private var accessFlags = method.accessFlags
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 _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
@@ -38,25 +35,25 @@ class MutableMethod(method: Method) :
this.returnType = returnType
}
override fun getDefiningClass(): String = definingClass
override fun getDefiningClass() = definingClass
override fun getName(): String = name
override fun getName() = name
override fun getParameterTypes(): MutableList<CharSequence> = _parameterTypes
override fun getParameterTypes() = _parameterTypes
override fun getReturnType(): String = returnType
override fun getReturnType() = returnType
override fun getAnnotations(): MutableSet<MutableAnnotation> = _annotations
override fun getAnnotations() = _annotations
override fun getAccessFlags(): Int = accessFlags
override fun getAccessFlags() = accessFlags
override fun getHiddenApiRestrictions(): MutableSet<HiddenApiRestriction> = _hiddenApiRestrictions
override fun getHiddenApiRestrictions() = _hiddenApiRestrictions
override fun getParameters(): MutableList<MutableMethodParameter> = _parameters
override fun getParameters() = _parameters
override fun getImplementation(): MutableMethodImplementation? = _implementation
override fun getImplementation() = _implementation
companion object {
fun Method.toMutable(): MutableMethod = MutableMethod(this)
fun Method.toMutable() = MutableMethod(this)
}
}

View File

@@ -0,0 +1,26 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.BaseMethodParameter
import com.android.tools.smali.dexlib2.iface.MethodParameter
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
class MutableMethodParameter(parameter: MethodParameter) : BaseMethodParameter(), MethodParameter {
private var type = parameter.type
private var name = parameter.name
private var signature = parameter.signature
private val _annotations by lazy {
parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
}
override fun getType() = type
override fun getName() = name
override fun getSignature() = signature
override fun getAnnotations() = _annotations
companion object {
fun MethodParameter.toMutable() = MutableMethodParameter(this)
}
}

View File

@@ -0,0 +1,17 @@
package java.io
internal expect fun File.kmpResolve(child: String): File
internal fun File.resolve(child: String) = kmpResolve(child)
internal expect fun File.kmpDeleteRecursively(): Boolean
internal fun File.deleteRecursively() = kmpDeleteRecursively()
internal expect fun File.kmpInputStream(): InputStream
internal fun File.inputStream() = kmpInputStream()
internal expect fun File.kmpBufferedWriter(charset: java.nio.charset.Charset): BufferedWriter
internal fun File.bufferedWriter(charset: java.nio.charset.Charset) = kmpBufferedWriter(charset)

View File

@@ -0,0 +1,26 @@
package app.revanced.patcher.patch
import java.io.File
import java.net.URLClassLoader
import java.util.function.Predicate
import java.util.jar.JarFile
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Loads patches from JAR files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @return The loaded patches.
*/
actual fun loadPatches(patchesFiles: Set<File>) = loadPatches(
patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
)

View File

@@ -0,0 +1,7 @@
package collections
internal actual fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V
) = merge(key, value, remappingFunction)

View File

@@ -0,0 +1,9 @@
package java.io
import java.nio.charset.Charset
internal actual fun File.kmpResolve(child: String) = resolve(child)
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
internal actual fun File.kmpInputStream() = inputStream()
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)

View File

@@ -1,7 +1,7 @@
package app.revanced.patcher
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode

View File

@@ -3,17 +3,23 @@ package app.revanced.patcher
import app.revanced.patcher.extensions.toInstructions
import app.revanced.patcher.patch.*
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.spyk
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import lanchon.multidexlib2.MultiDexIO
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertThrows
import java.util.logging.Logger
@@ -21,11 +27,14 @@ import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
internal object PatcherTest {
private lateinit var patcher: Patcher
private lateinit var patcherContext: PatcherContext
@BeforeEach
@JvmStatic
@BeforeAll
fun setUp() {
patcher = mockk<Patcher> {
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
@@ -40,9 +49,73 @@ internal object PatcherTest {
Logger.getAnonymousLogger(),
)
every { context.bytecodeContext.classDefs } returns mockk(relaxed = true)
every { this@mockk() } answers { callOriginal() }
}
val classDefs = mutableSetOf(
ImmutableClassDef(
"class",
0,
null,
null,
null,
null,
null,
listOf(
ImmutableMethod(
"class",
"method",
emptyList(),
"V",
0,
null,
null,
ImmutableMethodImplementation(
2,
"""
const-string v0, "Hello, World!"
iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
return-void
const-string v0, "This is a test."
return-object v0
invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J
check-cast p0, Ljava/io/PrintStream;
""".toInstructions(),
null,
null
),
),
),
)
)
patcherContext = mockk<PatcherContext> {
every { bytecodeContext } returns mockk<BytecodePatchContext> context@{
every { config } returns mockk<PatcherConfig> {
every { apkFile } returns mockk()
}
mockkStatic(MultiDexIO::readDexFile)
every {
MultiDexIO.readDexFile(
any(),
any(),
any(),
any(),
any()
)
} returns mockk<DexFile> {
every { classes } returns classDefs
every { opcodes } returns Opcodes.getDefault()
}
every { this@context.classDefs } returns ClassDefs().apply { initializeCache() }
every { mergeExtension(any<BytecodePatch>()) } just runs
}
}
every { patcher.context } returns patcherContext
}
@Test
@@ -156,11 +229,13 @@ internal object PatcherTest {
}
@Test
fun `throws if unmatched fingerprint match is delegated`() {
fun `throws if unmatched fingerprint match is used`() {
val patch = bytecodePatch {
execute {
// Fingerprint can never match.
val fingerprint = fingerprint { }
val fingerprint = fingerprint {
strings("doesnt exist")
}
// Throws, because the fingerprint can't be matched.
fingerprint.patternMatch
@@ -175,14 +250,16 @@ internal object PatcherTest {
val iterable = (1..10).toList()
val matcher = indexedMatcher<Int>()
matcher.apply { head { this > 5 } }
matcher.apply {
+head { this > 5 }
}
assertFalse(
matcher(iterable),
"Should not match at any other index than first"
)
matcher.clear()
matcher.apply { head { this == 1 } }(iterable)
matcher.apply { +head { this == 1 } }(iterable)
assertEquals(
listOf(0),
matcher.indices,
@@ -190,11 +267,11 @@ internal object PatcherTest {
)
matcher.clear()
matcher.apply { add { this > 0 } }(iterable)
matcher.apply { add { _, _ -> this > 0 } }(iterable)
assertEquals(1, matcher.indices.size, "Should only match once.")
matcher.clear()
matcher.apply { add { this == 2 } }(iterable)
matcher.apply { add { _, _ -> this == 2 } }(iterable)
assertEquals(
listOf(1),
matcher.indices,
@@ -203,9 +280,9 @@ internal object PatcherTest {
matcher.clear()
matcher.apply {
head { this == 1 }
add { this == 2 }
add { this == 4 }
+head { this == 1 }
add { _, _ -> this == 2 }
add { _, _ -> this == 4 }
}(iterable)
assertEquals(
listOf(0, 1, 3),
@@ -215,7 +292,7 @@ internal object PatcherTest {
matcher.clear()
matcher.apply {
after { this == 1 }
+after { this == 1 }
}(iterable)
assertEquals(
listOf(0),
@@ -225,7 +302,7 @@ internal object PatcherTest {
matcher.clear()
matcher.apply {
after(2..Int.MAX_VALUE) { this == 1 }
+after(2..Int.MAX_VALUE) { this == 1 }
}
assertFalse(
matcher(iterable),
@@ -234,7 +311,7 @@ internal object PatcherTest {
matcher.clear()
matcher.apply {
after(1..1) { this == 2 }
+after(1..1) { this == 2 }
}
assertFalse(
matcher(iterable),
@@ -243,10 +320,10 @@ internal object PatcherTest {
matcher.clear()
matcher.apply {
head { this == 1 }
after(2..5) { this == 4 }
add { this == 8 }
add { this == 9 }
+head { this == 1 }
+after(2..5) { this == 4 }
add { _, _ -> this == 8 }
add { _, _ -> this == 9 }
}(iterable)
assertEquals(
listOf(0, 3, 7, 8),
@@ -256,93 +333,55 @@ internal object PatcherTest {
}
@Test
fun `matches fingerprint`() {
every { patcher.context.bytecodeContext.classDefs } returns mutableSetOf(
ImmutableClassDef(
"class",
0,
null,
null,
null,
null,
null,
listOf(
ImmutableMethod(
"class",
"method",
emptyList(),
"V",
0,
null,
null,
ImmutableMethodImplementation(
2,
"""
const-string v0, "Hello, World!"
iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
return-void
const-string v0, "This is a test."
return-object v0
invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J
check-cast p0, Ljava/io/PrintStream;
""".toInstructions(),
null,
null
),
),
),
),
)
every {
with(patcher.context.bytecodeContext) {
any(ClassDef::class).mutable()
fun `matches via composite`() {
fun composite(fail: Boolean = false) = firstMethodComposite {
name("method")
definingClass("class")
if (fail) returnType("doesnt exist")
instructions(
head(Opcode.CONST_STRING()),
`is`<TwoRegisterInstruction>(),
noneOf(registers()),
string("test", String::contains),
after(1..3, allOf(Opcode.INVOKE_VIRTUAL(), registers(1, 0))),
allOf(),
type("PrintStream;", String::endsWith)
)
}
with(patcher.context.bytecodeContext) {
assertNotNull(composite().methodOrNull) {
"Expected to find a method matching the composite fingerprint."
}
} answers { callOriginal() }
val a = gettingFirstMethodOrNull { true }
assertNull(composite(fail = true).methodOrNull) {
"Expected to not find a method matching the composite fingerprint."
}
}
}
@Test
fun `matches fingerprint`() {
val fingerprint = fingerprint { returns("V") }
val fingerprint2 = fingerprint { returns("V") }
val fingerprint3 = fingerprint { returns("V") }
val composite = firstMethodComposite {
instructions {
head { opcode == Opcode.CONST_STRING }
add { opcode == Opcode.IPUT_OBJECT }
}
}
val patches = setOf(
bytecodePatch {
execute {
fingerprint.match(classDefs.first().methods.first())
fingerprint2.match(classDefs.first())
fingerprint3.originalClassDef
composite.method
}
},
)
patches()
with(patcher.context.bytecodeContext)
{
with(patcher.context.bytecodeContext) {
assertAll(
"Expected fingerprints to match.",
{ assertNotNull(fingerprint.originalClassDefOrNull) },
{ assertNotNull(fingerprint2.originalClassDefOrNull) },
{ assertNotNull(fingerprint.matchOrNull(this.classDefs.first().methods.first())) },
{ assertNotNull(fingerprint2.matchOrNull(this.classDefs.first())) },
{ assertNotNull(fingerprint3.originalClassDefOrNull) },
{ assertEquals("method", composite.method.name) },
)
}
}
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()
every { patcher.context.bytecodeContext.lookupMaps } returns with(patcher.context.bytecodeContext) { LookupMaps() }
every { with(patcher.context.bytecodeContext) { mergeExtension(any<BytecodePatch>()) } } just runs
// TODO: Ideally most of this mocking could be moved to setUp,
// but by doing so the mocking breaks and it is not clear why.
every { patcherContext.executablePatches } returns toMutableSet()
return runBlocking {
patcher().toList().also { results ->

View File

@@ -2,6 +2,7 @@
package app.revanced.patcher.patch
import io.mockk.mockk
import org.junit.jupiter.api.Test
import java.io.File
import kotlin.reflect.KFunction
@@ -47,44 +48,23 @@ private fun privateNamedPatchFunction() = privatePatch
// endregion
internal object PatchLoaderTest {
private const val LOAD_PATCHES_FUNCTION_NAME = "loadPatches"
private val TEST_PATCHES_CLASS = ::publicPatch.javaField!!.declaringClass.name
private val TEST_PATCHES_CLASS_LOADER = ::publicPatch.javaClass.classLoader
@Test
fun `loads patches correctly`() {
// Get instance of private PatchLoader.Companion class.
val patchLoaderCompanionObject = getPrivateFieldByType(
PatchLoader::class.java,
PatchLoader::class.companionObject!!.javaObjectType,
val patches = loadPatches(
setOf(mockk<File>()),
{ listOf(TEST_PATCHES_CLASS) },
TEST_PATCHES_CLASS_LOADER
)
// Get private PatchLoader.Companion.loadPatches function from PatchLoader.Companion.
@Suppress("UNCHECKED_CAST")
val loadPatchesFunction = getPrivateFunctionByName(
patchLoaderCompanionObject,
LOAD_PATCHES_FUNCTION_NAME,
) as KFunction<Map<File, Set<Patch<*>>>>
// Call private PatchLoader.Companion.loadPatches function.
val patches = loadPatchesFunction.call(
patchLoaderCompanionObject,
TEST_PATCHES_CLASS_LOADER,
mapOf(File("patchesFile") to setOf(TEST_PATCHES_CLASS)),
).values.first()
assertEquals(
2,
patches.size,
"Expected 2 patches to be loaded, " +
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
)
}
private fun getPrivateFieldByType(cls: Class<*>, fieldType: Class<*>) =
cls.declaredFields.first { it.type == fieldType }.apply { isAccessible = true }.get(null)
private fun getPrivateFunctionByName(obj: Any, @Suppress("SameParameterValue") methodName: String) =
obj::class.declaredFunctions.first { it.name == methodName }.apply { isAccessible = true }
}

View File

@@ -1,6 +1,6 @@
package app.revanced.patcher.util
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode

View File

@@ -1,3 +1,11 @@
org.gradle.parallel = true
org.gradle.caching = true
version = 22.0.0
version = 22.0.0-local
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.caching=true
org.gradle.configuration-cache=true
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true

View File

@@ -1,8 +1,10 @@
[versions]
android = "4.1.1.4"
apktool-lib = "2.10.1.1"
binary-compatibility-validator = "0.18.1"
agp = "8.12.3"
android-compileSdk = "36"
android-minSdk = "26"
kotlin = "2.2.21"
apktool-lib = "2.10.1.1"
kotlinx-coroutines-core = "1.10.2"
mockk = "1.14.6"
multidexlib2 = "3.0.3.r3"
@@ -10,9 +12,9 @@ multidexlib2 = "3.0.3.r3"
#noinspection GradleDependency
smali = "3.0.9"
xpp3 = "1.1.4c"
vanniktechMavenPublish = "0.35.0"
[libraries]
android = { module = "com.google.android:android", version.ref = "android" }
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
@@ -23,5 +25,6 @@ smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
[plugins]
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }

Binary file not shown.

12
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -115,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -173,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -206,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

3
gradlew.bat vendored
View File

@@ -70,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -1 +1,24 @@
rootProject.name = "revanced-patcher"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
maven {
name = "githubPackages"
// A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials(PasswordCredentials::class)
}
}
}
include(":core")

View File

@@ -1,784 +0,0 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package app.revanced.patcher
import app.revanced.patcher.InstructionLocation.*
import app.revanced.patcher.Match.PatternMatch
import app.revanced.patcher.Matcher.MatchContext
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.extensions.opcode
import app.revanced.patcher.extensions.string
import app.revanced.patcher.extensions.stringReference
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.util.MethodUtil
/**
* A fingerprint for a method. A fingerprint is a partial description of a method.
* It is used to uniquely match a method by its characteristics.
*
* An example fingerprint for a public method that takes a single string parameter and returns void:
* ```
* fingerprint {
* accessFlags(AccessFlags.PUBLIC)
* returns("V")
* parameters("Ljava/lang/String;")
* }
* ```
*
* @param accessFlags The exact access flags using values of [AccessFlags].
* @param returnType The return type. Compared using [String.startsWith].
* @param parameters The parameters. Partial matches allowed and follow the same rules as [returnType].
* @param filters A list of filters to match, declared in the same order the instructions appear in the method.
* @param strings A list of the strings that appear anywhere in the method. Compared using [String.contains].
* @param custom A custom condition for this fingerprint.
*/
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List<String>?,
internal val filters: List<InstructionFilter>?,
@Deprecated("Instead use instruction filters")
internal val strings: List<String>?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
) {
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
/**
* Clears the current match, forcing this fingerprint to resolve again.
* This method should only be used if this fingerprint is re-used after it's modified,
* and the prior match indexes are no longer correct.
*/
fun clearMatch() {
_matchOrNull = null
}
/**
* The match for this [Fingerprint], or `null` if no matches exist.
*/
context(context: BytecodePatchContext)
fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
var stringMatches: List<Match.StringMatch>? = null
val matchIndices = indexedMatcher<Instruction>()
// This line is needed, because the method must be passed by reference to "matchIndices".
// Referencing the method directly would "hardcode" it in the cached pattern by value.
// By using this variable, the reference can be updated for each method.
lateinit var currentMethod: Method
context(_: MatchContext)
fun Method.match(): Boolean {
if (this@Fingerprint.accessFlags != null && this@Fingerprint.accessFlags != accessFlags)
return false
if (this@Fingerprint.returnType != null && !returnType.startsWith(this@Fingerprint.returnType))
return false
if (this@Fingerprint.parameters != null && !parametersStartsWith(
parameterTypes,
this@Fingerprint.parameters
)
)
return false
if (custom != null && !custom(this, context.lookupMaps.classDefsByType[definingClass]!!))
return false
stringMatches = if (strings != null) {
val instructions = instructionsOrNull ?: return false
var stringsList: MutableList<String>? = null
buildList {
instructions.forEachIndexed { instructionIndex, instruction ->
if (stringsList == null) stringsList = strings.toMutableList()
val string = instruction.stringReference?.string ?: return@forEachIndexed
val index = stringsList.indexOfFirst(string::contains)
if (index < 0) return@forEachIndexed
add(Match.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList == null || stringsList.isNotEmpty()) return false
}
} else null
currentMethod = this
return filters == null || matchIndices(instructionsOrNull ?: return false) {
filters.forEach { filter ->
val filterMatches: Instruction.() -> Boolean = { filter.matches(currentMethod, this) }
when (val location = filter.location) {
is MatchAfterImmediately -> after { filterMatches() }
is MatchAfterWithin -> after(1..location.matchDistance) { filterMatches() }
is MatchAfterAnywhere -> add { filterMatches() }
is MatchAfterAtLeast -> after(location.minimumDistanceFromLastInstruction..Int.MAX_VALUE) { filterMatches() }
is MatchAfterRange -> after(location.minimumDistanceFromLastInstruction..location.maximumDistanceFromLastInstruction) { filterMatches() }
is MatchFirst -> head { filterMatches() }
}
}
}
}
val allStrings = buildList {
if (filters != null) addAll(
(filters.filterIsInstance<StringFilter>() + filters.filterIsInstance<AnyInstruction>().flatMap {
it.filters.filterIsInstance<StringFilter>()
})
)
}.map { it.stringValue } + (strings ?: emptyList())
val method = if (allStrings.isNotEmpty()) {
context.firstMethodOrNull(strings = allStrings.toTypedArray()) { match() }
?: context(MatchContext()) { context.lookupMaps.methodsWithString.firstOrNull { it.match() } }
} else {
context.firstMethodOrNull { match() }
} ?: return null
val instructionMatches = filters?.withIndex()?.map { (i, filter) ->
val matchIndex = matchIndices.indices[i]
Match.InstructionMatch(filter, matchIndex, method.getInstruction(matchIndex))
}
_matchOrNull = Match(
context,
context.lookupMaps.classDefsByType[method.definingClass]!!,
method,
instructionMatches,
stringMatches,
)
return _matchOrNull
}
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the
* fingerprint is already matched to a method, null otherwise.
*/
context(_: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): Match? {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
val match = matchOrNull(classDef, method)
if (match != null) {
_matchOrNull = match
return match
}
}
return null
}
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
* otherwise `null`.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
): Match? {
if (_matchOrNull != null) return _matchOrNull
return matchOrNull(context.lookupMaps.classDefsByType[method.definingClass]!!, method)
}
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
* otherwise `null`.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
method: Method,
): Match? {
if (_matchOrNull != null) return _matchOrNull
var stringMatches: List<Match.StringMatch>? = null
val matchIndices = indexedMatcher<Instruction>()
context(_: MatchContext)
fun Method.match(): Boolean {
if (this@Fingerprint.accessFlags != null && this@Fingerprint.accessFlags != accessFlags)
return false
if (this@Fingerprint.returnType != null && !returnType.startsWith(this@Fingerprint.returnType))
return false
if (this@Fingerprint.parameters != null && !parametersStartsWith(
parameterTypes,
this@Fingerprint.parameters
)
)
return false
if (custom != null && !custom(this, classDef))
return false
stringMatches = if (strings != null) {
val instructions = instructionsOrNull ?: return false
var stringsList: MutableList<String>? = null
buildList {
instructions.forEachIndexed { instructionIndex, instruction ->
if (stringsList == null) stringsList = strings.toMutableList()
val string = instruction.stringReference?.string ?: return@forEachIndexed
val index = stringsList.indexOfFirst(string::contains)
if (index < 0) return@forEachIndexed
add(Match.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList == null || stringsList.isNotEmpty()) return false
}
} else null
return filters == null || matchIndices(instructionsOrNull ?: return false) {
filters.forEach { filter ->
val filterMatches: Instruction.() -> Boolean = { filter.matches(method, this) }
when (val location = filter.location) {
is MatchAfterImmediately -> after { filterMatches() }
is MatchAfterWithin -> after(1..location.matchDistance) { filterMatches() }
is MatchAfterAnywhere -> add { filterMatches() }
is MatchAfterAtLeast -> after(location.minimumDistanceFromLastInstruction..Int.MAX_VALUE) { filterMatches() }
is MatchAfterRange -> after(location.minimumDistanceFromLastInstruction..location.maximumDistanceFromLastInstruction) { filterMatches() }
is MatchFirst -> head { filterMatches() }
}
}
}
}
if (!context(MatchContext()) { method.match() }) return null
val instructionMatches = filters?.withIndex()?.map { (i, filter) ->
val matchIndex = matchIndices.indices[i]
Match.InstructionMatch(filter, matchIndex, method.getInstruction(matchIndex))
}
_matchOrNull = Match(
context,
context.lookupMaps.classDefsByType[method.definingClass]!!,
method,
instructionMatches,
stringMatches,
)
return _matchOrNull
}
fun patchException() = PatchException("Failed to match the fingerprint: $this")
/**
* The match for this [Fingerprint].
*
* @return The [Match] of this fingerprint.
* @throws PatchException If the [Fingerprint] failed to match.
*/
context(_: BytecodePatchContext)
fun match() = matchOrNull() ?: throw patchException()
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] of this fingerprint.
* @throws PatchException If the fingerprint failed to match.
*/
context(_: BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw patchException()
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] of this fingerprint.
* @throws PatchException If the fingerprint failed to match.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw patchException()
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] of this fingerprint.
* @throws PatchException If the fingerprint failed to match.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(classDef, method) ?: throw patchException()
/**
* The class the matching method is a member of, or null if this fingerprint did not match.
*/
context(_: BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull()?.originalClassDef
/**
* The matching method, or null of this fingerprint did not match.
*/
context(_: BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull()?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
* Accessing this property allocates a new mutable instance.
* Use [originalClassDefOrNull] if mutable access is not required.
*/
context(_: BytecodePatchContext)
val classDefOrNull
get() = matchOrNull()?.classDef
/**
* The mutable version of [originalMethodOrNull].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(_: BytecodePatchContext)
val methodOrNull
get() = matchOrNull()?.method
/**
* The match for the opcode pattern, or null if this fingerprint did not match.
*/
context(_: BytecodePatchContext)
@Deprecated("instead use instructionMatchesOrNull")
val patternMatchOrNull: PatternMatch?
get() {
val match = this.matchOrNull()
if (match == null || match.instructionMatchesOrNull == null) {
return null
}
return match.patternMatch
}
/**
* The match for the instruction filters, or null if this fingerprint did not match.
*/
context(_: BytecodePatchContext)
val instructionMatchesOrNull
get() = matchOrNull()?.instructionMatchesOrNull
/**
* The matches for the strings, or null if this fingerprint did not match.
*
* This does not give matches for strings declared using [string] instruction filters.
*/
context(_: BytecodePatchContext)
@Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
val stringMatchesOrNull
get() = matchOrNull()?.stringMatchesOrNull
/**
* The class the matching method is a member of.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val originalClassDef
get() = match().originalClassDef
/**
* The matching method.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val originalMethod
get() = match().originalMethod
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a new mutable instance.
* Use [originalClassDef] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val classDef
get() = match().classDef
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val method
get() = match().method
/**
* The match for the opcode pattern.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
@Deprecated("Instead use instructionMatch")
val patternMatch
get() = match().patternMatch
/**
* Instruction filter matches.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val instructionMatches
get() = match().instructionMatches
/**
* The matches for the strings declared using `strings()`.
* This does not give matches for strings declared using [string] instruction filters.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
@Deprecated("Instead use string instructions and `instructionMatches()`")
val stringMatches
get() = match().stringMatches
}
/**
* A match of a [Fingerprint].
*
* @param originalClassDef The class the matching method is a member of.
* @param originalMethod The matching method.
* @param _instructionMatches The match for the instruction filters.
* @param _stringMatches The matches for the strings declared using `strings()`.
*/
class Match internal constructor(
val context: BytecodePatchContext,
val originalClassDef: ClassDef,
val originalMethod: Method,
private val _instructionMatches: List<InstructionMatch>?,
private val _stringMatches: List<StringMatch>?,
) {
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a new mutable instance.
* Use [originalClassDef] if mutable access is not required.
*/
val classDef by lazy { with(context) { originalClassDef.mutable() } }
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*/
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
@Deprecated("Instead use instructionMatches", ReplaceWith("instructionMatches"))
val patternMatch by lazy {
if (_instructionMatches == null) throw PatchException("Did not match $this")
@SuppressWarnings("deprecation")
PatternMatch(_instructionMatches.first().index, _instructionMatches.last().index)
}
val instructionMatches
get() = _instructionMatches ?: throw PatchException("Fingerprint declared no instruction filters")
val instructionMatchesOrNull = _instructionMatches
@Deprecated("Instead use string instructions and `instructionMatches()`")
val stringMatches
get() = _stringMatches ?: throw PatchException("Fingerprint declared no strings")
@Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
val stringMatchesOrNull = _stringMatches
/**
* A match for an opcode pattern.
* @param startIndex The index of the first opcode of the pattern in the method.
* @param endIndex The index of the last opcode of the pattern in the method.
*/
@Deprecated("Instead use InstructionMatch")
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
)
/**
* A match for a string.
*
* @param string The string that matched.
* @param index The index of the instruction in the method.
*/
@Deprecated("Instead use string instructions and `InstructionMatch`")
class StringMatch internal constructor(val string: String, val index: Int)
/**
* A match for a [InstructionFilter].
* @param filter The filter that matched
* @param index The instruction index it matched with.
* @param instruction The instruction that matched.
*/
class InstructionMatch internal constructor(
val filter: InstructionFilter,
val index: Int,
val instruction: Instruction
) {
@Suppress("UNCHECKED_CAST")
fun <T> getInstruction(): T = instruction as T
}
}
/**
* A builder for [Fingerprint].
*
* @property accessFlags The exact access flags using values of [AccessFlags].
* @property returnType The return type compared using [String.startsWith].
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @property instructionFilters Filters to match the method instructions.
* @property strings A list of the strings compared each using [String.contains].
* @property customBlock A custom condition for this fingerprint.
*
* @constructor Create a new [FingerprintBuilder].
*/
class FingerprintBuilder() {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List<String>? = null
private var instructionFilters: List<InstructionFilter>? = null
private var strings: List<String>? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(vararg accessFlags: AccessFlags) {
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
}
/**
* Set the return type.
*
* If [accessFlags] includes [AccessFlags.CONSTRUCTOR], then there is no need to
* set a return type set since constructors are always void return type.
*
* @param returnType The return type compared using [String.startsWith].
*/
fun returns(returnType: String) {
this.returnType = returnType
}
/**
* Set the parameters.
*
* @param parameters The parameters of the method.
* Partial matches allowed and follow the same rules as [returnType].
*/
fun parameters(vararg parameters: String) {
this.parameters = parameters.toList()
}
private fun verifyNoFiltersSet() {
if (this.instructionFilters != null) {
throw PatchException("Instruction filters already set")
}
}
/**
* A pattern of opcodes, where each opcode must appear immediately after the previous.
*
* To use opcodes with other [InstructionFilter] objects,
* instead use [instructions] with individual opcodes declared using [opcode].
*
* This method is identical to declaring individual opcode filters
* with [InstructionFilter.location] set to [InstructionLocation.MatchAfterImmediately]
* for all but the first opcode.
*
* Unless absolutely necessary, it is recommended to instead use [instructions]
* with more fine grained filters.
*
* ```
* opcodes(
* Opcode.INVOKE_VIRTUAL, // First opcode matches anywhere in the method.
* Opcode.MOVE_RESULT_OBJECT, // Must match exactly after INVOKE_VIRTUAL.
* Opcode.IPUT_OBJECT // Must match exactly after MOVE_RESULT_OBJECT.
* )
* ```
* is identical to:
* ```
* instructions(
* opcode(Opcode.INVOKE_VIRTUAL), // First opcode matches anywhere in the method.
* opcode(Opcode.MOVE_RESULT_OBJECT, maxAfter = 0), // Must match exactly after INVOKE_VIRTUAL.
* opcode(Opcode.IPUT_OBJECT, maxAfter = 0) // Must match exactly after MOVE_RESULT_OBJECT.
* )
* ```
*
* @param opcodes An opcode pattern of instructions.
* Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
verifyNoFiltersSet()
if (opcodes.isEmpty()) throw IllegalArgumentException("One or more opcodes is required")
this.instructionFilters = OpcodesFilter.listOfOpcodes(opcodes.toList())
}
/**
* A pattern of opcodes from SMALI formatted text,
* where each opcode must appear immediately after the previous opcode.
*
* Unless absolutely necessary, it is recommended to instead use [instructions].
*
* @param instructions A list of instructions or opcode names in SMALI format.
* - Wildcard or unknown opcodes can be specified by `null`.
* - Empty lines are ignored.
* - Each instruction must be on a new line.
* - The opcode name is enough, no need to specify the operands.
*
* @throws Exception If an unknown opcode is used.
*/
fun opcodes(instructions: String) {
verifyNoFiltersSet()
if (instructions.isBlank()) throw IllegalArgumentException("No instructions declared (empty string)")
this.instructionFilters = OpcodesFilter.listOfOpcodes(
instructions.trimIndent().split("\n").filter {
it.isNotBlank()
}.map {
// Remove any operands.
val name = it.split(" ", limit = 1).first().trim()
if (name == "null") return@map null
opcodesByName[name] ?: throw IllegalArgumentException("Unknown opcode: $name")
}
)
}
/**
* A list of instruction filters to match.
*/
fun instructions(vararg instructionFilters: InstructionFilter) {
verifyNoFiltersSet()
if (instructionFilters.isEmpty()) throw IllegalArgumentException("One or more instructions is required")
this.instructionFilters = instructionFilters.toList()
}
/**
* Set the strings.
*
* @param strings A list of strings compared each using [String.contains].
*/
@Deprecated("Instead use `instruction()` filters and `string()` instruction declarations")
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
/**
* Set a custom condition for this fingerprint.
*
* @param customBlock A custom condition for this fingerprint.
*/
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
this.customBlock = customBlock
}
fun build(): Fingerprint {
// If access flags include constructor then
// skip the return type check since it's always void.
if (returnType?.equals("V") == true && accessFlags != null
&& AccessFlags.CONSTRUCTOR.isSet(accessFlags!!)
) {
returnType = null
}
return Fingerprint(
accessFlags,
returnType,
parameters,
instructionFilters,
strings,
customBlock,
)
}
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
fun fingerprint(
block: FingerprintBuilder.() -> Unit,
) = FingerprintBuilder().apply(block).build()
/**
* Matches two lists of parameters, where the first parameter list
* starts with the values of the second list.
*/
internal fun parametersStartsWith(
targetMethodParameters: Iterable<CharSequence>,
fingerprintParameters: Iterable<CharSequence>,
): Boolean {
if (fingerprintParameters.count() != targetMethodParameters.count()) return false
val fingerprintIterator = fingerprintParameters.iterator()
targetMethodParameters.forEach {
if (!it.startsWith(fingerprintIterator.next())) return false
}
return true
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,582 +0,0 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate", "CONTEXT_RECEIVERS_DEPRECATED")
package app.revanced.patcher
import app.revanced.patcher.Matcher.MatchContext
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.extensions.accessFlags
import app.revanced.patcher.patch.BytecodePatchContext
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.HiddenApiRestriction
import com.android.tools.smali.dexlib2.iface.*
import com.android.tools.smali.dexlib2.iface.Annotation
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.util.MethodUtil
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
fun Iterable<ClassDef>.anyClassDef(predicate: ClassDef.() -> Boolean) = any(predicate)
fun ClassDef.anyMethod(predicate: Method.() -> Boolean) = methods.any(predicate)
fun ClassDef.anyDirectMethod(predicate: Method.() -> Boolean) = directMethods.any(predicate)
fun ClassDef.anyVirtualMethod(predicate: Method.() -> Boolean) = virtualMethods.any(predicate)
fun ClassDef.anyField(predicate: Field.() -> Boolean) = fields.any(predicate)
fun ClassDef.anyInstanceField(predicate: Field.() -> Boolean) = instanceFields.any(predicate)
fun ClassDef.anyStaticField(predicate: Field.() -> Boolean) = staticFields.any(predicate)
fun ClassDef.anyInterface(predicate: String.() -> Boolean) = interfaces.any(predicate)
fun ClassDef.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate)
fun Method.implementation(predicate: MethodImplementation.() -> Boolean) = implementation?.predicate() ?: false
fun Method.anyParameter(predicate: MethodParameter.() -> Boolean) = parameters.any(predicate)
fun Method.anyParameterType(predicate: CharSequence.() -> Boolean) = parameterTypes.any(predicate)
fun Method.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate)
fun Method.anyHiddenApiRestriction(predicate: HiddenApiRestriction.() -> Boolean) = hiddenApiRestrictions.any(predicate)
fun MethodImplementation.anyInstruction(predicate: Instruction.() -> Boolean) = instructions.any(predicate)
fun MethodImplementation.anyTryBlock(predicate: TryBlock<out ExceptionHandler>.() -> Boolean) = tryBlocks.any(predicate)
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate)
fun Iterable<Instruction>.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate)
fun BytecodePatchContext.firstClassDefOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
with(MatchContext()) { classDefs.firstOrNull { it.predicate() } }
fun BytecodePatchContext.firstClassDef(predicate: context(MatchContext) ClassDef.() -> Boolean) =
requireNotNull(firstClassDefOrNull(predicate))
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
firstClassDefOrNull(predicate)?.mutable()
fun BytecodePatchContext.firstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
requireNotNull(firstClassDefMutableOrNull(predicate))
fun BytecodePatchContext.firstClassDefOrNull(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = lookupMaps.classDefsByType[type]?.takeIf {
predicate == null || with(MatchContext()) { it.predicate() }
}
fun BytecodePatchContext.firstClassDef(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(firstClassDefOrNull(type, predicate))
fun BytecodePatchContext.firstClassDefMutableOrNull(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = firstClassDefOrNull(type, predicate)?.mutable()
fun BytecodePatchContext.firstClassDefMutable(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(firstClassDefMutableOrNull(type, predicate))
fun Iterable<ClassDef>.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
with(MatchContext()) {
this@firstMethodOrNull.asSequence().flatMap { it.methods.asSequence() }.firstOrNull { it.predicate() }
}
fun Iterable<ClassDef>.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(firstMethodOrNull(predicate))
/** Can't compile due to JVM platform declaration clash
fun Iterable<Method>.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
with(MatchContext()) { firstOrNull { it.predicate() } }
fun Iterable<Method>.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
with(MatchContext()) { requireNotNull(firstMethodOrNull(predicate)) }
**/
fun BytecodePatchContext.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
classDefs.firstMethodOrNull(predicate)
fun BytecodePatchContext.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(firstMethodOrNull(predicate))
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
classDefs.firstMethodOrNull(predicate)?.let { method ->
lookupMaps.classDefsByType[method.definingClass]!!.mutable().methods.first {
MethodUtil.methodSignaturesMatch(method, it)
}
}
fun BytecodePatchContext.firstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(firstMethodMutableOrNull(predicate))
fun BytecodePatchContext.firstMethodOrNull(
vararg strings: String,
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = with(MatchContext()) {
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.firstOrNull { it.predicate() }
}
fun BytecodePatchContext.firstMethod(
vararg strings: String,
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
fun BytecodePatchContext.firstMethodMutableOrNull(
vararg strings: String,
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = with(MatchContext()) {
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.let { methods ->
methods.firstOrNull { it.predicate() }?.let { method ->
firstClassDefMutable(method.definingClass).methods.first {
MethodUtil.methodSignaturesMatch(
method, it
)
}
}
}
}
fun BytecodePatchContext.firstMethodMutable(
vararg strings: String, predicate: context(MatchContext) Method.() -> Boolean = { true }
) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate))
class CachedReadOnlyProperty<T> internal constructor(
private val block: BytecodePatchContext.(KProperty<*>) -> T
) : ReadOnlyProperty<BytecodePatchContext, T> {
private var value: T? = null
private var cached = false
override fun getValue(thisRef: BytecodePatchContext, property: KProperty<*>): T {
if (!cached) {
value = thisRef.block(property)
cached = true
}
return value!!
}
}
fun gettingFirstClassDefOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDefOrNull(predicate) }
fun gettingFirstClassDef(predicate: context(MatchContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDef(predicate) }
fun gettingFirstClassDefMutableOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDefMutableOrNull(predicate) }
fun gettingFirstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
CachedReadOnlyProperty { firstClassDefMutable(predicate) }
fun gettingFirstClassDefOrNull(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDefOrNull(type, predicate) }
fun gettingFirstClassDef(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDef(type, predicate) }
fun gettingFirstClassDefMutableOrNull(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDefMutableOrNull(type, predicate) }
fun gettingFirstClassDefMutable(
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = CachedReadOnlyProperty { firstClassDefMutable(type, predicate) }
fun gettingFirstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethodOrNull(predicate) }
fun gettingFirstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethod(predicate) }
fun gettingFirstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethodMutableOrNull(predicate) }
fun gettingFirstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
CachedReadOnlyProperty { firstMethodMutable(predicate) }
fun gettingFirstMethodOrNull(
vararg strings: String,
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethodOrNull(*strings, predicate = predicate) }
fun gettingFirstMethod(
vararg strings: String,
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethod(*strings, predicate = predicate) }
fun gettingFirstMethodMutableOrNull(
vararg strings: String,
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethodMutableOrNull(*strings, predicate = predicate) }
fun gettingFirstMethodMutable(
vararg strings: String,
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = CachedReadOnlyProperty { firstMethodMutable(*strings, predicate = predicate) }
fun <T> indexedMatcher() = IndexedMatcher<T>()
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
IndexedMatcher<T>().apply(build)
fun <T> Iterable<T>.matchIndexed(build: IndexedMatcher<T>.() -> Unit) =
indexedMatcher(build)(this)
context(_: MatchContext)
fun <T> Iterable<T>.matchIndexed(key: Any, build: IndexedMatcher<T>.() -> Unit) =
indexedMatcher<T>()(key, this, build)
context(_: MatchContext)
operator fun <T> IndexedMatcher<T>.invoke(key: Any, iterable: Iterable<T>, builder: IndexedMatcher<T>.() -> Unit) =
remember(key) { apply(builder) }(iterable)
context(_: MatchContext)
operator fun <T> IndexedMatcher<T>.invoke(iterable: Iterable<T>, builder: IndexedMatcher<T>.() -> Unit) =
invoke(this@invoke.hashCode(), iterable, builder)
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
var matchIndex = -1
protected set
abstract operator fun invoke(haystack: Iterable<T>): Boolean
class MatchContext internal constructor() : MutableMap<Any, Any> by mutableMapOf()
}
context(context: MatchContext)
inline fun <reified V : Any> remember(key: Any, defaultValue: () -> V) =
context[key] as? V ?: defaultValue().also { context[key] = it }
class IndexedMatcher<T>() : Matcher<T, T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean>() {
private val _indices: MutableList<Int> = mutableListOf()
val indices: List<Int> = _indices
private var lastMatchedIndex = -1
private var currentIndex = -1
private var nextIndex: Int? = null
override fun invoke(haystack: Iterable<T>): Boolean {
// Normalize to list
val hay = haystack as? List<T> ?: haystack.toList()
_indices.clear()
this@IndexedMatcher.lastMatchedIndex = -1
currentIndex = -1
data class Frame(
val patternIndex: Int,
val lastMatchedIndex: Int,
val previousFrame: Frame?,
var nextHayIndex: Int,
val matchedIndex: Int
)
val stack = ArrayDeque<Frame>()
stack.add(
Frame(
patternIndex = 0,
lastMatchedIndex = -1,
previousFrame = null,
nextHayIndex = 0,
matchedIndex = -1
)
)
while (stack.isNotEmpty()) {
val frame = stack.last()
if (frame.nextHayIndex >= hay.size || nextIndex == -1) {
stack.removeLast()
nextIndex = null
continue
}
val i = frame.nextHayIndex
currentIndex = i
lastMatchedIndex = frame.lastMatchedIndex
nextIndex = null
if (this[frame.patternIndex](hay[i], lastMatchedIndex, currentIndex)) {
Frame(
patternIndex = frame.patternIndex + 1,
lastMatchedIndex = i,
previousFrame = frame,
nextHayIndex = i + 1,
matchedIndex = i
).also {
if (it.patternIndex == size) {
_indices += buildList(size) {
var f: Frame? = it
while (f != null && f.matchedIndex != -1) {
add(f.matchedIndex)
f = f.previousFrame
}
}.asReversed()
return true
}
}.let(stack::add)
}
frame.nextHayIndex = when (val nextIndex = nextIndex) {
null -> frame.nextHayIndex + 1
-1 -> 0 // Frame will be removed next loop.
else -> nextIndex
}
}
return false
}
fun head(predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean) =
add { lastMatchedIndex, currentIndex ->
currentIndex == 0 && predicate(lastMatchedIndex, currentIndex)
}
fun head(predicate: T.() -> Boolean) =
head { _, _ -> predicate() }
fun after(range: IntRange = 1..1, predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean) =
add { lastMatchedIndex, currentIndex ->
val distance = currentIndex - lastMatchedIndex
nextIndex = when {
distance < range.first -> lastMatchedIndex + range.first
distance > range.last -> -1
else -> return@add predicate(lastMatchedIndex, currentIndex)
}
false
}
fun after(range: IntRange = 1..1, predicate: T.() -> Boolean) =
after(range) { _, _ -> predicate() }
fun add(predicate: T.() -> Boolean) = add { _, _ -> predicate() }
}
fun <T> T.declarativePredicate(build: DeclarativePredicateBuilder<T>.() -> Unit) =
DeclarativePredicateBuilder<T>().apply(build).all(this)
context(_: MatchContext)
fun <T> T.rememberDeclarativePredicate(key: Any, block: DeclarativePredicateBuilder<T>.() -> Unit): Boolean =
remember(key) { DeclarativePredicateBuilder<T>().apply(block) }.all(this)
context(_: MatchContext)
private fun <T> T.rememberDeclarativePredicate(predicate: context(MatchContext, T) DeclarativePredicateBuilder<T>.() -> Unit) =
rememberDeclarativePredicate("declarative predicate build") { predicate() }
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefOrNull { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefMutableOrNull { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefOrNull(type) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(type, predicate))
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefMutableOrNull(type) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(type, predicate))
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodOrNull { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodMutableOrNull { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate))
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodMutableOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate))
fun gettingFirstClassDefByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefOrNull(type) { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(type, predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefMutableOrNull(type) { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(type, predicate) }
fun gettingFirstClassDefByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefByDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefMutableOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = CachedReadOnlyProperty { firstClassDefMutableByDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicateOrNull(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicate(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(predicate = predicate) }
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodMutableOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodMutableByDeclarativePredicate(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(predicate = predicate) }
fun gettingFirstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodByDeclarativePredicate(*strings, predicate = predicate) }
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodMutableOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = CachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(*strings, predicate = predicate) }
class DeclarativePredicateBuilder<T> internal constructor() {
private val children = mutableListOf<T.() -> Boolean>()
fun anyOf(block: DeclarativePredicateBuilder<T>.() -> Unit) {
val child = DeclarativePredicateBuilder<T>().apply(block)
children += { child.children.any { it() } }
}
fun predicate(block: T.() -> Boolean) {
children += block
}
fun all(target: T): Boolean = children.all { target.it() }
fun any(target: T): Boolean = children.all { target.it() }
}
fun firstMethodComposite(
predicate:
context(MatchContext, Method, IndexedMatcher<Instruction>) DeclarativePredicateBuilder<Method>.() -> Unit
) = with(indexedMatcher<Instruction>()) { Composition(indices = this.indices) { predicate() } }
fun DeclarativePredicateBuilder<Method>.accessFlags(vararg flags: AccessFlags) {
predicate { accessFlags(*flags) }
}
fun DeclarativePredicateBuilder<Method>.returns(returnType: String) {
predicate { this.returnType.startsWith(returnType) }
}
fun DeclarativePredicateBuilder<Method>.parameterTypes(vararg parameterTypes: String) = predicate {
this.parameterTypes.size == parameterTypes.size && this.parameterTypes.zip(parameterTypes)
.all { (a, b) -> a.startsWith(b) }
}
context(_: MatchContext, indexedMatcher: IndexedMatcher<Instruction>)
fun DeclarativePredicateBuilder<Method>.instructions(
build: context(MatchContext, Method) IndexedMatcher<Instruction>.() -> Unit
) = predicate { implementation { indexedMatcher(indexedMatcher.hashCode(), instructions) { build() } } }
context(_: MatchContext)
fun DeclarativePredicateBuilder<Method>.custom(block: context(MatchContext) Method.() -> Boolean) {
predicate { block() }
}
class Composition internal constructor(
val indices: List<Int>,
private val predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) {
private var _methodOrNull: MutableMethod? = null
context(context: BytecodePatchContext)
val methodOrNull: MutableMethod?
get() {
if (_methodOrNull == null) {
_methodOrNull = context.firstMethodMutableByDeclarativePredicateOrNull(predicate)
}
return _methodOrNull
}
context(_: BytecodePatchContext)
val method get() = requireNotNull(methodOrNull)
}

View File

@@ -1,29 +0,0 @@
package app.revanced.patcher.dex.mutable
import app.revanced.patcher.dex.mutable.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.base.BaseMethodParameter
import com.android.tools.smali.dexlib2.iface.MethodParameter
// TODO: finish overriding all members if necessary
class MutableMethodParameter(parameter: MethodParameter) :
BaseMethodParameter(),
MethodParameter {
private var type = parameter.type
private var name = parameter.name
private var signature = parameter.signature
private val _annotations by lazy {
parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
}
override fun getType(): String = type
override fun getName(): String? = name
override fun getSignature(): String? = signature
override fun getAnnotations(): MutableSet<MutableAnnotation> = _annotations
companion object {
fun MethodParameter.toMutable(): MutableMethodParameter = MutableMethodParameter(this)
}
}

View File

@@ -1,20 +0,0 @@
package app.revanced.patcher.dex.mutable.encodedValue
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.base.value.BaseArrayEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ArrayEncodedValue
import com.android.tools.smali.dexlib2.iface.value.EncodedValue
class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) :
BaseArrayEncodedValue(),
MutableEncodedValue {
private val _value by lazy {
arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
}
override fun getValue(): MutableList<out EncodedValue> = _value
companion object {
fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue = MutableArrayEncodedValue(this)
}
}

View File

@@ -1,12 +0,0 @@
package app.revanced.patcher.dex.mutable.encodedValue
import com.android.tools.smali.dexlib2.base.value.BaseNullEncodedValue
import com.android.tools.smali.dexlib2.iface.value.ByteEncodedValue
class MutableNullEncodedValue :
BaseNullEncodedValue(),
MutableEncodedValue {
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}
}

View File

@@ -1,228 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.dex.mutable.MutableClassDef
import app.revanced.patcher.dex.mutable.MutableClassDef.Companion.toMutable
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO
import java.io.Closeable
import java.io.IOException
import java.util.LinkedHashMap
import java.util.logging.Logger
/**
* A context for patches containing the current state of the bytecode.
*
* @param config The [PatcherConfig] used to create this context.
*/
@Suppress("MemberVisibilityCanBePrivate")
class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable {
private val logger = Logger.getLogger(this::class.java.name)
/**
* [Opcodes] of the supplied [PatcherConfig.apkFile].
*/
internal val opcodes: Opcodes
/**
* The list of classes.
*/
val classDefs = MultiDexIO.readDexFile(
true,
config.apkFile,
BasicDexFileNamer(),
null,
null,
).also { opcodes = it.opcodes }.classes.toMutableSet()
/**
* The lookup maps for methods and the class they are a member of from the [classDefs].
*/
internal val lookupMaps by lazy { _lookupMaps ?: LookupMaps().also { _lookupMaps = it } }
private var _lookupMaps: LookupMaps? = null // For freeing up memory when compiling.
/**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
* If no extension is present, the function will return early.
*
* @param bytecodePatch The [BytecodePatch] to merge the extension of.
*/
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
val existingClass = lookupMaps.classDefsByType[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" }
classDefs += classDef
lookupMaps += classDef
return@forEach
}
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) {
return@let
}
classDefs -= existingClass
lookupMaps -= existingClass
classDefs += mergedClass
lookupMaps += mergedClass
}
}
} ?: logger.fine("No extension to merge")
}
/**
* Convert a [ClassDef] to a [MutableClassDef].
* If the [ClassDef] is already a [MutableClassDef], it is returned as is.
*
* @return The mutable version of the [ClassDef].
*/
fun ClassDef.mutable(): MutableClassDef = this as? MutableClassDef ?: also {
classDefs -= this
lookupMaps -= this
}.toMutable().also {
classDefs += it
lookupMaps += it
}
/**
* Navigate a method.
*
* @param method The method to navigate.
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: MethodReference) = MethodNavigator(method)
/**
* Compile bytecode from the [BytecodePatchContext].
*
* @return The compiled bytecode.
*/
@InternalApi
override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
// Free up memory before compiling the dex files.
close()
System.gc()
val patchedDexFileResults =
config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
-1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = this@BytecodePatchContext.classDefs.let {
// More performant according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/commit/8c26ad08457fb1565ea5794b7930da42a1c81cf1#diff-be698366d9868784ecf7da3fd4ac9d2b335b0bb637f9f618fbe067dbd6830b8fR197
// TODO: Benchmark, if actually faster.
HashSet<ClassDef>(it.size * 3 / 2).apply { addAll(it) }
}
override fun getOpcodes() = this@BytecodePatchContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
}.listFiles { it.isFile }!!.map {
PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
// Free up more memory, although it is unclear if this is actually helpful.
classDefs.clear()
System.gc()
return patchedDexFileResults
}
override fun close() {
try {
_lookupMaps = null
} catch (e: IOException) {
logger.warning("Failed to clear BytecodePatchContext: ${e.message}")
}
}
internal inner class LookupMaps {
// No custom HashMap needed here, according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/commit/9b6d95d4f414a35ed68da37b0ecd8549df1ef63a
// TODO: Benchmark, if actually faster.
private val _classDefsByType = mutableMapOf<String, ClassDef>()
val classDefsByType: Map<String, ClassDef> = _classDefsByType
// Better performance according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/commit/9b6d95d4f414a35ed68da37b0ecd8549df1ef63a
private val _methodsByStrings =
LinkedHashMap<String, MutableList<Method>>(2 * classDefs.size, 0.5f)
val methodsByStrings: Map<String, List<Method>> = _methodsByStrings
private val _methodsWithString = methodsByStrings.values.flatten().toMutableSet()
val methodsWithString: Set<Method> = _methodsWithString
init {
classDefs.forEach(::plusAssign)
}
private fun ClassDef.forEachString(action: (Method, String) -> Unit) = methods.asSequence().forEach { method ->
method.instructionsOrNull?.asSequence()
?.filterIsInstance<ReferenceInstruction>()
?.map { it.reference }
?.filterIsInstance<StringReference>()
?.map { it.string }
?.forEach { string ->
action(method, string)
}
}
operator fun plusAssign(classDef: ClassDef) {
_classDefsByType[classDef.type] = classDef
classDef.forEachString { method, string ->
_methodsWithString += method
_methodsByStrings.getOrPut(string) {
mutableListOf()
} += method
}
}
operator fun minusAssign(classDef: ClassDef) {
_classDefsByType -= classDef.type
classDef.forEachString { method, string ->
_methodsWithString.remove(method)
if (_methodsByStrings[string]?.also { it -= method }?.isEmpty() == true)
_methodsByStrings -= string
}
}
}
}

View File

@@ -1 +0,0 @@
version=${projectVersion}