mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-27 05:01:03 +00:00
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:
@@ -1,3 +0,0 @@
|
||||
[*.{kt,kts}]
|
||||
ktlint_code_style = intellij_idea
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
||||
133
.gitignore
vendored
133
.gitignore
vendored
@@ -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
|
||||
|
||||
122
build.gradle.kts
122
build.gradle.kts
@@ -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
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
109
core/build.gradle.kts
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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)
|
||||
8
core/src/androidMain/kotlin/java/io/File.android.kt
Normal file
8
core/src/androidMain/kotlin/java/io/File.android.kt
Normal 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)
|
||||
598
core/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt
Normal file
598
core/src/commonMain/kotlin/app/revanced/patcher/Fingerprint.kt
Normal 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()
|
||||
814
core/src/commonMain/kotlin/app/revanced/patcher/Matching.kt
Normal file
814
core/src/commonMain/kotlin/app/revanced/patcher/Matching.kt
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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].
|
||||
@@ -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.
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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.
|
||||
13
core/src/commonMain/kotlin/collections/MutableMap.kt
Normal file
13
core/src/commonMain/kotlin/collections/MutableMap.kt
Normal 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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
17
core/src/commonMain/kotlin/java/io/File.kt
Normal file
17
core/src/commonMain/kotlin/java/io/File.kt
Normal 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)
|
||||
@@ -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()),
|
||||
)
|
||||
7
core/src/jvmMain/kotlin/collections/MutableMap.jvm.kt
Normal file
7
core/src/jvmMain/kotlin/collections/MutableMap.jvm.kt
Normal 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)
|
||||
9
core/src/jvmMain/kotlin/java/io/File.jvm.kt
Normal file
9
core/src/jvmMain/kotlin/java/io/File.jvm.kt
Normal 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)
|
||||
@@ -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
|
||||
@@ -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 ->
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
12
gradlew
vendored
12
gradlew
vendored
@@ -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
3
gradlew.bat
vendored
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
version=${projectVersion}
|
||||
Reference in New Issue
Block a user