You've already forked revanced-patcher
mirror of
https://github.com/revanced/revanced-patcher
synced 2025-09-06 16:38:50 +02:00
Compare commits
163 Commits
v13.0.0-de
...
v19.0.0-de
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f05a404e48 | ||
![]() |
a46e948b5a | ||
![]() |
dc09ea639f | ||
![]() |
49ed096e85 | ||
![]() |
167bd83f4e | ||
![]() |
aed1eac315 | ||
![]() |
54a2f8f16f | ||
![]() |
2ca543ffb9 | ||
![]() |
58e7f815a5 | ||
![]() |
15b38fc841 | ||
![]() |
e2ca50729d | ||
![]() |
6192089b71 | ||
![]() |
a4212f6bf9 | ||
![]() |
124a2e9d3e | ||
![]() |
f77624b3b9 | ||
![]() |
a76ac04214 | ||
![]() |
0447fa9c28 | ||
![]() |
54ac1394a9 | ||
![]() |
0b04c73ac5 | ||
![]() |
079de45238 | ||
![]() |
56ce9ec2f9 | ||
![]() |
1b52e4b0f9 | ||
![]() |
098c2c1efa | ||
![]() |
64343e5a7c | ||
![]() |
0caf6caeb9 | ||
![]() |
55f6c2a9fc | ||
![]() |
c6095bc38a | ||
![]() |
09cd6aa568 | ||
![]() |
ebbaafb78e | ||
![]() |
e6de90d300 | ||
![]() |
c7922e90d0 | ||
![]() |
c1f4c0445a | ||
![]() |
caa634fac6 | ||
![]() |
f28bfe0dbd | ||
![]() |
155e787ff4 | ||
![]() |
c38f0ef42a | ||
![]() |
4456031459 | ||
![]() |
5fb59a227f | ||
![]() |
d9fb241d57 | ||
![]() |
642c4ea97e | ||
![]() |
8c8a251626 | ||
![]() |
5953d6cfb5 | ||
![]() |
a1962fe600 | ||
![]() |
77dbee3d6a | ||
![]() |
cb5e39d73e | ||
![]() |
38ef2f470a | ||
![]() |
129d84e108 | ||
![]() |
affeba76b8 | ||
![]() |
6059d3ca26 | ||
![]() |
444dee5a16 | ||
![]() |
d314466ce2 | ||
![]() |
fdaf9c21c8 | ||
![]() |
06c2b76f11 | ||
![]() |
3896b30738 | ||
![]() |
2c4b88e1a0 | ||
![]() |
dfc7e1596b | ||
![]() |
f590436399 | ||
![]() |
cbfb9ba02f | ||
![]() |
b4cfe80ad5 | ||
![]() |
b37906fa35 | ||
![]() |
356f1f1553 | ||
![]() |
e882af74ee | ||
![]() |
46875fb28e | ||
![]() |
417c3e4234 | ||
![]() |
6d2c28807b | ||
![]() |
4d6e08a650 | ||
![]() |
5cebc1fd30 | ||
![]() |
ac61731dc6 | ||
![]() |
9e4ffabd5c | ||
![]() |
3f410bd39f | ||
![]() |
d51bc32e37 | ||
![]() |
b7f6aa94cc | ||
![]() |
ff965e6953 | ||
![]() |
468d5d7421 | ||
![]() |
fc95b28c49 | ||
![]() |
69184187d9 | ||
![]() |
a802d0df46 | ||
![]() |
8de30633ae | ||
![]() |
a1fbb7990f | ||
![]() |
aa71146b1b | ||
![]() |
9fdb8f087f | ||
![]() |
670f0153de | ||
![]() |
1d7aeca696 | ||
![]() |
4e7811ea07 | ||
![]() |
e11283744a | ||
![]() |
91cdfd53ef | ||
![]() |
bc7d6b9941 | ||
![]() |
6b1e0a1656 | ||
![]() |
72c9eb2129 | ||
![]() |
4bc4b0dc01 | ||
![]() |
637d48746f | ||
![]() |
9a109c129b | ||
![]() |
d49e4ee5ea | ||
![]() |
30f0ea29a3 | ||
![]() |
49930f6565 | ||
![]() |
909d89fa8d | ||
![]() |
81d1d7f544 | ||
![]() |
67b7dff67a | ||
![]() |
4b76d19596 | ||
![]() |
080fbe9feb | ||
![]() |
d3721229bf | ||
![]() |
86c1c9c772 | ||
![]() |
c299817193 | ||
![]() |
fcc1de45ed | ||
![]() |
a29931f2ec | ||
![]() |
3fc6a139ee | ||
![]() |
4dd04975d9 | ||
![]() |
3b4db3ddb7 | ||
![]() |
c4a7117ee8 | ||
![]() |
b4e900fde8 | ||
![]() |
9818d730e4 | ||
![]() |
11a3378659 | ||
![]() |
1bb05f22d3 | ||
![]() |
26b70554c4 | ||
![]() |
93b29d2e83 | ||
![]() |
072986374a | ||
![]() |
2c590d212a | ||
![]() |
6cc863efb3 | ||
![]() |
b832812767 | ||
![]() |
c44558cacd | ||
![]() |
6d83a720cd | ||
![]() |
8d0dd9c448 | ||
![]() |
64020eec49 | ||
![]() |
4dedfb85cb | ||
![]() |
55d694579a | ||
![]() |
86db64edff | ||
![]() |
983563efb6 | ||
![]() |
37abb2db99 | ||
![]() |
5ba0b47e60 | ||
![]() |
e8f2087a6f | ||
![]() |
6ce99f5cdf | ||
![]() |
13c0c9cdd3 | ||
![]() |
58ffdb60d7 | ||
![]() |
ba56a6a2ee | ||
![]() |
ccccf5b1d2 | ||
![]() |
b507ac0a54 | ||
![]() |
e985676c2d | ||
![]() |
f7f4ba6c55 | ||
![]() |
4292f43814 | ||
![]() |
30bd4fd9fe | ||
![]() |
76de39369d | ||
![]() |
88a703ce36 | ||
![]() |
5938f6b7ea | ||
![]() |
5c0c0d6c37 | ||
![]() |
0f15077225 | ||
![]() |
273dd8d388 | ||
![]() |
1795f376ef | ||
![]() |
e7360a7692 | ||
![]() |
e1fc86934f | ||
![]() |
6b8977f178 | ||
![]() |
12c6c73de0 | ||
![]() |
db62a1607b | ||
![]() |
58bb879ef5 | ||
![]() |
254912438a | ||
![]() |
0e48918bcc | ||
![]() |
783ccf8529 | ||
![]() |
8fb2f2dc1d | ||
![]() |
2a8cc283c7 | ||
![]() |
433fe3af9f | ||
![]() |
c2d89c622e | ||
![]() |
02d6ff15fe | ||
![]() |
f2cb7ee7df | ||
![]() |
a2ac44dcc1 |
6
.github/workflows/pull_request.yml
vendored
6
.github/workflows/pull_request.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: PR to main
|
||||
name: Open a PR to main
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
|
||||
jobs:
|
||||
pull-request:
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew clean --no-daemon
|
||||
run: ./gradlew build clean --no-daemon
|
||||
- name: Setup semantic-release
|
||||
run: npm install
|
||||
- name: Release
|
||||
|
564
CHANGELOG.md
564
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
770
api/revanced-patcher.api
Normal file
770
api/revanced-patcher.api
Normal file
File diff suppressed because it is too large
Load Diff
104
build.gradle.kts
104
build.gradle.kts
@@ -1,50 +1,48 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.8.20"
|
||||
kotlin("jvm") version "1.9.10"
|
||||
alias(libs.plugins.binary.compatibility.validator)
|
||||
`maven-publish`
|
||||
signing
|
||||
java
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
|
||||
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
listOf("multidexlib2", "apktool").forEach { repo ->
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/$repo")
|
||||
credentials {
|
||||
username = githubUsername
|
||||
password = githubPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("xpp3:xpp3:1.1.4c")
|
||||
implementation("com.android.tools.smali:smali:3.0.3")
|
||||
implementation("app.revanced:multidexlib2:3.0.3.r2")
|
||||
implementation("app.revanced:apktool-lib:2.8.2-2")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
||||
}
|
||||
|
||||
tasks {
|
||||
processResources {
|
||||
expand("projectVersion" to project.version)
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("PASSED", "SKIPPED", "FAILED")
|
||||
}
|
||||
}
|
||||
processResources {
|
||||
expand("projectVersion" to project.version)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.xpp3)
|
||||
implementation(libs.smali)
|
||||
implementation(libs.multidexlib2)
|
||||
implementation(libs.apktool.lib)
|
||||
implementation(libs.kotlin.reflect)
|
||||
|
||||
compileOnly(libs.android)
|
||||
|
||||
testImplementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
@@ -53,22 +51,36 @@ kotlin {
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (System.getenv("GITHUB_ACTOR") != null)
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
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"
|
||||
}
|
||||
}
|
||||
else
|
||||
mavenLocal()
|
||||
}
|
||||
publications {
|
||||
register<MavenPublication>("gpr") {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
kotlin.code.style = official
|
||||
version = 13.0.0-dev.1
|
||||
version = 19.0.0-dev.1
|
||||
|
23
gradle/libs.versions.toml
Normal file
23
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[versions]
|
||||
android = "4.1.1.4"
|
||||
kotlin-reflect = "1.9.0"
|
||||
apktool-lib = "2.9.1"
|
||||
kotlin-test = "1.8.20-RC"
|
||||
kotlinx-coroutines-core = "1.7.3"
|
||||
multidexlib2 = "3.0.3.r3"
|
||||
smali = "3.0.3"
|
||||
xpp3 = "1.1.4c"
|
||||
binary-compatibility-validator = "0.13.2"
|
||||
|
||||
[libraries]
|
||||
android = { module = "com.google.android:android", version.ref = "android" }
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||
apktool-lib = { module = "app.revanced:apktool", version.ref = "apktool-lib" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
||||
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
||||
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" }
|
@@ -1 +1 @@
|
||||
rootProject.name = "revanced-patcher"
|
||||
rootProject.name = "revanced-patcher"
|
@@ -0,0 +1,8 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import java.io.File
|
||||
|
||||
@FunctionalInterface
|
||||
interface IntegrationsConsumer {
|
||||
fun acceptIntegrations(integrations: List<File>)
|
||||
}
|
@@ -1,15 +1,14 @@
|
||||
package app.revanced.patcher.data
|
||||
package app.revanced.patcher
|
||||
|
||||
import brut.androlib.apk.ApkInfo
|
||||
|
||||
/**
|
||||
* Metadata about a package.
|
||||
*/
|
||||
class PackageMetadata {
|
||||
class PackageMetadata internal constructor(internal val apkInfo: ApkInfo) {
|
||||
lateinit var packageName: String
|
||||
internal set
|
||||
|
||||
lateinit var packageVersion: String
|
||||
internal set
|
||||
|
||||
internal lateinit var apkInfo: ApkInfo
|
||||
}
|
127
src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt
Normal file
127
src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt
Normal file
@@ -0,0 +1,127 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import dalvik.system.DexClassLoader
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarFile
|
||||
import java.util.logging.Logger
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* A set of [Patch]es.
|
||||
*/
|
||||
typealias PatchSet = Set<Patch<*>>
|
||||
|
||||
/**
|
||||
* A [Patch] class.
|
||||
*/
|
||||
typealias PatchClass = KClass<out Patch<*>>
|
||||
|
||||
/**
|
||||
* A loader of [Patch]es from patch bundles.
|
||||
* This will load all [Patch]es from the given patch bundles that have a name.
|
||||
*
|
||||
* @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle.
|
||||
* @param classLoader The [ClassLoader] to use for loading the classes.
|
||||
* @param patchBundles A set of patches to initialize this instance with.
|
||||
*/
|
||||
sealed class PatchBundleLoader private constructor(
|
||||
classLoader: ClassLoader,
|
||||
patchBundles: Array<out File>,
|
||||
getBinaryClassNames: (patchBundle: File) -> List<String>,
|
||||
// This constructor parameter is unfortunately necessary,
|
||||
// so that a reference to the mutable set is present in the constructor to be able to add patches to it.
|
||||
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
|
||||
private val patchSet: MutableSet<Patch<*>> = mutableSetOf()
|
||||
) : PatchSet by patchSet {
|
||||
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
|
||||
|
||||
init {
|
||||
patchBundles.flatMap(getBinaryClassNames).asSequence().map {
|
||||
classLoader.loadClass(it)
|
||||
}.filter {
|
||||
Patch::class.java.isAssignableFrom(it)
|
||||
}.mapNotNull { patchClass ->
|
||||
patchClass.getInstance(logger, silent = true)
|
||||
}.filter {
|
||||
it.name != null
|
||||
}.let { patches ->
|
||||
patchSet.addAll(patches)
|
||||
}
|
||||
}
|
||||
|
||||
internal companion object Utils {
|
||||
/**
|
||||
* Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used.
|
||||
*
|
||||
* @param logger The [Logger] to use for logging.
|
||||
* @param silent Whether to suppress logging.
|
||||
* @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated.
|
||||
*/
|
||||
internal fun Class<*>.getInstance(logger: Logger, silent: Boolean = false): Patch<*>? {
|
||||
return try {
|
||||
getField("INSTANCE").get(null)
|
||||
} catch (exception: NoSuchFieldException) {
|
||||
if (!silent) logger.fine(
|
||||
"Patch class '${name}' has no INSTANCE field, therefor not a singleton. " +
|
||||
"Will try to instantiate it."
|
||||
)
|
||||
|
||||
try {
|
||||
getDeclaredConstructor().newInstance()
|
||||
} catch (exception: Exception) {
|
||||
if (!silent) logger.severe(
|
||||
"Patch class '${name}' is not singleton and has no suitable constructor, " +
|
||||
"therefor cannot be instantiated and will be ignored."
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
} as Patch<*>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PatchBundleLoader] for JAR files.
|
||||
*
|
||||
* @param patchBundles The path to patch bundles of JAR format.
|
||||
*/
|
||||
class Jar(vararg patchBundles: File) : PatchBundleLoader(
|
||||
URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()),
|
||||
patchBundles,
|
||||
{ patchBundle ->
|
||||
JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
|
||||
.map { it.name.replace('/', '.').replace(".class", "") }
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* A [PatchBundleLoader] for [Dex] files.
|
||||
*
|
||||
* @param patchBundles The path to patch bundles of DEX format.
|
||||
* @param optimizedDexDirectory The directory to store optimized DEX files in.
|
||||
* This parameter is deprecated and has no effect since API level 26.
|
||||
*/
|
||||
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
|
||||
DexClassLoader(
|
||||
patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath,
|
||||
null,
|
||||
PatchBundleLoader::class.java.classLoader
|
||||
),
|
||||
patchBundles,
|
||||
{ patchBundle ->
|
||||
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
|
||||
.map { classDef ->
|
||||
classDef.type.substring(1, classDef.length - 1)
|
||||
}
|
||||
}
|
||||
) {
|
||||
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
|
||||
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.PatchResult
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.function.Function
|
||||
|
||||
@FunctionalInterface
|
||||
interface PatchExecutorFunction : Function<Boolean, Flow<PatchResult>>
|
File diff suppressed because it is too large
Load Diff
@@ -1,64 +1,41 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.*
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import java.io.File
|
||||
import brut.androlib.apk.ApkInfo
|
||||
import brut.directory.ExtFile
|
||||
|
||||
data class PatcherContext(
|
||||
val classes: MutableList<ClassDef>,
|
||||
val resourceCacheDirectory: File,
|
||||
) {
|
||||
val packageMetadata = PackageMetadata()
|
||||
internal val patches = mutableListOf<Class<out Patch<Context>>>()
|
||||
internal val integrations = Integrations(this)
|
||||
internal val bytecodeContext = BytecodeContext(classes)
|
||||
internal val resourceContext = ResourceContext(resourceCacheDirectory)
|
||||
/**
|
||||
* A context for ReVanced [Patcher].
|
||||
*
|
||||
* @param options The [PatcherOptions] used to create this context.
|
||||
*/
|
||||
class PatcherContext internal constructor(options: PatcherOptions) {
|
||||
/**
|
||||
* [PackageMetadata] of the supplied [PatcherOptions.inputFile].
|
||||
*/
|
||||
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile)))
|
||||
|
||||
internal class Integrations(val context: PatcherContext) {
|
||||
var callback: ((File) -> Unit)? = null
|
||||
private val integrations: MutableList<File> = mutableListOf()
|
||||
/**
|
||||
* The map of [Patch]es associated by their [PatchClass].
|
||||
*/
|
||||
internal val executablePatches = mutableMapOf<PatchClass, Patch<*>>()
|
||||
|
||||
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
|
||||
/**
|
||||
* The map of all [Patch]es and their dependencies associated by their [PatchClass].
|
||||
*/
|
||||
internal val allPatches = mutableMapOf<PatchClass, Patch<*>>()
|
||||
|
||||
/**
|
||||
* Merge integrations.
|
||||
* @param logger A logger.
|
||||
*/
|
||||
fun merge(logger: Logger) {
|
||||
with(context.bytecodeContext.classes) {
|
||||
for (integrations in integrations) {
|
||||
callback?.let { it(integrations) }
|
||||
/**
|
||||
* The [ResourceContext] of this [PatcherContext].
|
||||
* This holds the current state of the resources.
|
||||
*/
|
||||
internal val resourceContext = ResourceContext(this, options)
|
||||
|
||||
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile(
|
||||
true,
|
||||
integrations,
|
||||
NAMER,
|
||||
null,
|
||||
null
|
||||
).classes) {
|
||||
val type = classDef.type
|
||||
|
||||
val result = classes.findIndexed { it.type == type }
|
||||
if (result == null) {
|
||||
logger.trace("Merging type $type")
|
||||
classes.add(classDef)
|
||||
continue
|
||||
}
|
||||
|
||||
val (existingClass, existingClassIndex) = result
|
||||
|
||||
logger.trace("Type $type exists. Adding missing methods and fields.")
|
||||
|
||||
existingClass.merge(classDef, context, logger).let { mergedClass ->
|
||||
if (mergedClass !== existingClass) // referential equality check
|
||||
classes[existingClassIndex] = mergedClass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The [BytecodeContext] of this [PatcherContext].
|
||||
* This holds the current state of the bytecode.
|
||||
*/
|
||||
internal val bytecodeContext = BytecodeContext(options)
|
||||
}
|
16
src/main/kotlin/app/revanced/patcher/PatcherException.kt
Normal file
16
src/main/kotlin/app/revanced/patcher/PatcherException.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
/**
|
||||
* An exception thrown by ReVanced [Patcher].
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
* @param cause The corresponding [Throwable].
|
||||
*/
|
||||
sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
|
||||
constructor(errorMessage: String) : this(errorMessage, null)
|
||||
|
||||
|
||||
class CircularDependencyException internal constructor(dependant: String) : PatcherException(
|
||||
"Patch '$dependant' causes a circular dependency"
|
||||
)
|
||||
}
|
@@ -1,30 +1,49 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.logging.impl.NopLogger
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import brut.androlib.Config
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Options for the [Patcher].
|
||||
* @param inputFile The input file (usually an apk file).
|
||||
* @param resourceCacheDirectory Directory to cache resources.
|
||||
* @param aaptPath Optional path to a custom aapt binary.
|
||||
* @param frameworkDirectory Optional path to a custom framework directory.
|
||||
* @param logger Custom logger implementation for the [Patcher].
|
||||
* Options for ReVanced [Patcher].
|
||||
* @param inputFile The input file to patch.
|
||||
* @param resourceCachePath The path to the directory to use for caching resources.
|
||||
* @param aaptBinaryPath The path to a custom aapt binary.
|
||||
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
|
||||
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
|
||||
* This can impact memory usage.
|
||||
*/
|
||||
data class PatcherOptions(
|
||||
internal val inputFile: File,
|
||||
internal val resourceCacheDirectory: String,
|
||||
internal val aaptPath: String? = null,
|
||||
internal val frameworkDirectory: String? = null,
|
||||
internal val logger: Logger = NopLogger
|
||||
internal val resourceCachePath: File = File("revanced-resource-cache"),
|
||||
internal val aaptBinaryPath: String? = null,
|
||||
internal val frameworkFileDirectory: String? = null,
|
||||
internal val multithreadingDexFileWriter: Boolean = false,
|
||||
) {
|
||||
fun recreateResourceCacheDirectory() = File(resourceCacheDirectory).also {
|
||||
private val logger = Logger.getLogger(PatcherOptions::class.java.name)
|
||||
|
||||
/**
|
||||
* The mode to use for resource decoding.
|
||||
* @see ResourceContext.ResourceDecodingMode
|
||||
*/
|
||||
internal var resourceDecodingMode = ResourceContext.ResourceDecodingMode.MANIFEST_ONLY
|
||||
|
||||
/**
|
||||
* The configuration to use for resource decoding and compiling.
|
||||
*/
|
||||
internal val resourceConfig = Config.getDefaultConfig().apply {
|
||||
useAapt2 = true
|
||||
aaptPath = aaptBinaryPath ?: ""
|
||||
frameworkDirectory = frameworkFileDirectory
|
||||
}
|
||||
|
||||
fun recreateResourceCacheDirectory() = resourceCachePath.also {
|
||||
if (it.exists()) {
|
||||
logger.info("Deleting existing resource cache directory")
|
||||
|
||||
if (!it.deleteRecursively())
|
||||
logger.error("Failed to delete existing resource cache directory")
|
||||
logger.severe("Failed to delete existing resource cache directory")
|
||||
}
|
||||
|
||||
it.mkdirs()
|
||||
|
@@ -1,16 +1,23 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.util.dex.DexFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* The result of a patcher.
|
||||
* @param dexFiles The patched dex files.
|
||||
* @param doNotCompress List of relative paths to files to exclude from compressing.
|
||||
* @param resourceFile File containing resources that need to be extracted into the APK.
|
||||
* @param doNotCompress List of relative paths of files to exclude from compressing.
|
||||
*/
|
||||
data class PatcherResult(
|
||||
val dexFiles: List<DexFile>,
|
||||
val doNotCompress: List<String>? = null,
|
||||
val resourceFile: File?
|
||||
)
|
||||
val dexFiles: List<PatchedDexFile>,
|
||||
val resourceFile: File?,
|
||||
val doNotCompress: List<String>? = null
|
||||
) {
|
||||
/**
|
||||
* Wrapper for dex files.
|
||||
* @param name The original name of the dex file.
|
||||
* @param stream The dex file as [InputStream].
|
||||
*/
|
||||
class PatchedDexFile(val name: String, val stream: InputStream)
|
||||
}
|
8
src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt
Normal file
8
src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
@FunctionalInterface
|
||||
interface PatchesConsumer {
|
||||
fun acceptPatches(patches: List<Patch<*>>)
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
/**
|
||||
* Annotation to constrain a [Patch] to compatible packages.
|
||||
* @param compatiblePackages A list of packages a [Patch] is compatible with.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Compatibility(
|
||||
val compatiblePackages: Array<Package>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotation to represent packages a patch can be compatible with.
|
||||
* @param name The package identifier name.
|
||||
* @param versions The versions of the package the [Patch] is compatible with.
|
||||
*/
|
||||
@Target()
|
||||
annotation class Package(
|
||||
val name: String,
|
||||
val versions: Array<String> = [],
|
||||
)
|
@@ -1,32 +0,0 @@
|
||||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
/**
|
||||
* Annotation to name a [Patch].
|
||||
* @param name A suggestive name for the [Patch].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Name(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotation to describe a [Patch].
|
||||
* @param description A description for the [Patch].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Description(
|
||||
val description: String,
|
||||
)
|
||||
|
||||
|
||||
/**
|
||||
* Annotation to version a [Patch].
|
||||
* @param version The version of a [Patch].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Deprecated("This annotation is deprecated and will be removed in a future release.")
|
||||
annotation class Version(
|
||||
val version: String,
|
||||
)
|
168
src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt
Normal file
168
src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt
Normal file
@@ -0,0 +1,168 @@
|
||||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
import app.revanced.patcher.util.ProxyClassList
|
||||
import app.revanced.patcher.util.method.MethodWalker
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
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 lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.DexIO
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.Flushable
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for bytecode.
|
||||
* This holds the current state of the bytecode.
|
||||
*
|
||||
* @param options The [PatcherOptions] used to create this context.
|
||||
*/
|
||||
class BytecodeContext internal constructor(private val options: PatcherOptions) :
|
||||
Context<List<PatcherResult.PatchedDexFile>> {
|
||||
private val logger = Logger.getLogger(BytecodeContext::class.java.name)
|
||||
|
||||
/**
|
||||
* [Opcodes] of the supplied [PatcherOptions.inputFile].
|
||||
*/
|
||||
internal lateinit var opcodes: Opcodes
|
||||
|
||||
/**
|
||||
* The list of classes.
|
||||
*/
|
||||
val classes by lazy {
|
||||
ProxyClassList(
|
||||
MultiDexIO.readDexFile(
|
||||
true, options.inputFile, BasicDexFileNamer(), null, null
|
||||
).also { opcodes = it.opcodes }.classes.toMutableSet()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [Integrations] of this [PatcherContext].
|
||||
*/
|
||||
internal val integrations = Integrations()
|
||||
|
||||
/**
|
||||
* Find a class by a given class name.
|
||||
*
|
||||
* @param className The name of the class.
|
||||
* @return A proxy for the first class that matches the class name.
|
||||
*/
|
||||
fun findClass(className: String) = findClass { it.type.contains(className) }
|
||||
|
||||
/**
|
||||
* Find a class by a given predicate.
|
||||
*
|
||||
* @param predicate A predicate to match the class.
|
||||
* @return A proxy for the first class that matches the predicate.
|
||||
*/
|
||||
fun findClass(predicate: (ClassDef) -> Boolean) =
|
||||
// if we already proxied the class matching the predicate...
|
||||
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
|
||||
// else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||
classes.find(predicate)?.let { proxy(it) }
|
||||
|
||||
/**
|
||||
* Proxy a class.
|
||||
* This will allow the class to be modified.
|
||||
*
|
||||
* @param classDef The class to proxy.
|
||||
* @return A proxy for the class.
|
||||
*/
|
||||
fun proxy(classDef: ClassDef) = this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
|
||||
ClassProxy(classDef).also { this.classes.add(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||
*
|
||||
* @param startMethod The method to start at.
|
||||
* @return A [MethodWalker] instance.
|
||||
*/
|
||||
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
|
||||
|
||||
/**
|
||||
* Compile bytecode from the [BytecodeContext].
|
||||
*
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
override fun get(): List<PatcherResult.PatchedDexFile> {
|
||||
logger.info("Compiling patched dex files")
|
||||
|
||||
val patchedDexFileResults = options.resourceCachePath.resolve("dex").also {
|
||||
it.deleteRecursively() // Make sure the directory is empty.
|
||||
it.mkdirs()
|
||||
}.apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true,
|
||||
if (options.multithreadingDexFileWriter) -1 else 1,
|
||||
this,
|
||||
BasicDexFileNamer(),
|
||||
object : DexFile {
|
||||
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
|
||||
override fun getOpcodes() = this@BytecodeContext.opcodes
|
||||
},
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE
|
||||
) { _, entryName, _ -> logger.info("Compiled $entryName") }
|
||||
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
|
||||
|
||||
System.gc()
|
||||
|
||||
return patchedDexFileResults
|
||||
}
|
||||
|
||||
/**
|
||||
* The integrations of a [PatcherContext].
|
||||
*/
|
||||
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
|
||||
/**
|
||||
* Whether to merge integrations.
|
||||
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
|
||||
*/
|
||||
var merge = false
|
||||
|
||||
/**
|
||||
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
|
||||
*/
|
||||
override fun flush() {
|
||||
if (!merge) return
|
||||
|
||||
logger.info("Merging integrations")
|
||||
|
||||
val classMap = classes.associateBy { it.type }
|
||||
|
||||
this@Integrations.forEach { integrations ->
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
integrations, BasicDexFileNamer(),
|
||||
null,
|
||||
null
|
||||
).classes.forEach classDef@{ classDef ->
|
||||
val existingClass = classMap[classDef.type] ?: run {
|
||||
logger.fine("Adding $classDef")
|
||||
classes.add(classDef)
|
||||
return@classDef
|
||||
}
|
||||
|
||||
logger.fine("$classDef exists. Adding missing methods and fields.")
|
||||
|
||||
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
|
||||
// If the class was merged, replace the original class with the merged class.
|
||||
if (mergedClass === existingClass) return@let
|
||||
classes.apply { remove(existingClass); add(mergedClass) }
|
||||
}
|
||||
}
|
||||
}
|
||||
clear()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,170 +1,9 @@
|
||||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.util.ProxyBackedClassList
|
||||
import app.revanced.patcher.util.method.MethodWalker
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import org.w3c.dom.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext].
|
||||
* A common interface for contexts such as [ResourceContext] and [BytecodeContext].
|
||||
*/
|
||||
|
||||
sealed interface Context
|
||||
|
||||
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
|
||||
/**
|
||||
* The list of classes.
|
||||
*/
|
||||
val classes = ProxyBackedClassList(classes)
|
||||
|
||||
/**
|
||||
* Find a class by a given class name.
|
||||
*
|
||||
* @param className The name of the class.
|
||||
* @return A proxy for the first class that matches the class name.
|
||||
*/
|
||||
fun findClass(className: String) = findClass { it.type.contains(className) }
|
||||
|
||||
/**
|
||||
* Find a class by a given predicate.
|
||||
*
|
||||
* @param predicate A predicate to match the class.
|
||||
* @return A proxy for the first class that matches the predicate.
|
||||
*/
|
||||
fun findClass(predicate: (ClassDef) -> Boolean) =
|
||||
// if we already proxied the class matching the predicate...
|
||||
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
|
||||
// else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||
classes.find(predicate)?.let { proxy(it) }
|
||||
|
||||
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
|
||||
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
|
||||
if (proxy == null) {
|
||||
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
|
||||
this.classes.add(proxy)
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||
*
|
||||
* @param startMethod The method to start at.
|
||||
* @return A [MethodWalker] instance.
|
||||
*/
|
||||
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
|
||||
return MethodWalker(this, startMethod)
|
||||
}
|
||||
|
||||
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
|
||||
for ((index, element) in this.withIndex()) {
|
||||
if (predicate(element)) {
|
||||
return element to index
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
|
||||
val xmlEditor = XmlFileHolder()
|
||||
|
||||
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
|
||||
|
||||
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
|
||||
|
||||
inner class XmlFileHolder {
|
||||
operator fun get(inputStream: InputStream) =
|
||||
DomFileEditor(inputStream)
|
||||
|
||||
operator fun get(path: String): DomFileEditor {
|
||||
return DomFileEditor(this@ResourceContext[path])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for a file that can be edited as a dom document.
|
||||
*
|
||||
* This constructor does not check for locks to the file when writing.
|
||||
* Use the secondary constructor.
|
||||
*
|
||||
* @param inputStream the input stream to read the xml file from.
|
||||
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
|
||||
*
|
||||
*/
|
||||
class DomFileEditor internal constructor(
|
||||
private val inputStream: InputStream,
|
||||
private val outputStream: Lazy<OutputStream>? = null,
|
||||
) : Closeable {
|
||||
// path to the xml file to unlock the resource when closing the editor
|
||||
private var filePath: String? = null
|
||||
private var closed: Boolean = false
|
||||
|
||||
/**
|
||||
* The document of the xml file
|
||||
*/
|
||||
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
||||
.also(Document::normalize)
|
||||
|
||||
|
||||
// lazily open an output stream
|
||||
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
|
||||
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
|
||||
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
|
||||
// increase the lock
|
||||
locks.merge(file.path, 1, Integer::sum)
|
||||
filePath = file.path
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the editor. Write backs and decreases the lock count.
|
||||
*
|
||||
* Will not write back to the file if the file is still locked.
|
||||
*/
|
||||
override fun close() {
|
||||
if (closed) return
|
||||
|
||||
inputStream.close()
|
||||
|
||||
// if the output stream is not null, do not close it
|
||||
outputStream?.let {
|
||||
// prevent writing to same file, if it is being locked
|
||||
// isLocked will be false if the editor was created through a stream
|
||||
val isLocked = filePath?.let { path ->
|
||||
val isLocked = locks[path]!! > 1
|
||||
// decrease the lock count if the editor was opened for a file
|
||||
locks.merge(path, -1, Integer::sum)
|
||||
isLocked
|
||||
} ?: false
|
||||
|
||||
// if unlocked, write back to the file
|
||||
if (!isLocked) {
|
||||
it.value.use { stream ->
|
||||
val result = StreamResult(stream)
|
||||
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
|
||||
}
|
||||
|
||||
it.value.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
closed = true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// map of concurrent open files
|
||||
val locks = mutableMapOf<String, Int>()
|
||||
}
|
||||
}
|
||||
sealed interface Context<T> : Supplier<T>
|
170
src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt
Normal file
170
src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt
Normal file
@@ -0,0 +1,170 @@
|
||||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.util.DomFileEditor
|
||||
import brut.androlib.AaptInvoker
|
||||
import brut.androlib.ApkDecoder
|
||||
import brut.androlib.apk.UsesFramework
|
||||
import brut.androlib.res.Framework
|
||||
import brut.androlib.res.ResourcesDecoder
|
||||
import brut.androlib.res.decoder.AndroidManifestResourceParser
|
||||
import brut.androlib.res.decoder.XmlPullStreamDecoder
|
||||
import brut.androlib.res.xml.ResXmlPatcher
|
||||
import brut.directory.ExtFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for resources.
|
||||
* This holds the current state of the resources.
|
||||
*
|
||||
* @param context The [PatcherContext] to create the context for.
|
||||
*/
|
||||
class ResourceContext internal constructor(
|
||||
private val context: PatcherContext,
|
||||
private val options: PatcherOptions
|
||||
) : Context<File?>, Iterable<File> {
|
||||
private val logger = Logger.getLogger(ResourceContext::class.java.name)
|
||||
|
||||
val xmlEditor = XmlFileHolder()
|
||||
|
||||
/**
|
||||
* Decode resources for the patcher.
|
||||
*
|
||||
* @param mode The [ResourceDecodingMode] to use when decoding.
|
||||
*/
|
||||
internal fun decodeResources(mode: ResourceDecodingMode) = with(context.packageMetadata.apkInfo) {
|
||||
// Needed to decode resources.
|
||||
val resourcesDecoder = ResourcesDecoder(options.resourceConfig, this)
|
||||
|
||||
when (mode) {
|
||||
ResourceDecodingMode.FULL -> {
|
||||
val outDir = options.recreateResourceCacheDirectory()
|
||||
|
||||
logger.info("Decoding resources")
|
||||
|
||||
resourcesDecoder.decodeResources(outDir)
|
||||
resourcesDecoder.decodeManifest(outDir)
|
||||
|
||||
// Needed to record uncompressed files.
|
||||
val apkDecoder = ApkDecoder(options.resourceConfig, this)
|
||||
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
|
||||
|
||||
usesFramework = UsesFramework().apply {
|
||||
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
|
||||
}
|
||||
}
|
||||
|
||||
ResourceDecodingMode.MANIFEST_ONLY -> {
|
||||
logger.info("Decoding app manifest")
|
||||
|
||||
// Decode manually instead of using resourceDecoder.decodeManifest
|
||||
// because it does not support decoding to an OutputStream.
|
||||
XmlPullStreamDecoder(
|
||||
AndroidManifestResourceParser(resourcesDecoder.resTable),
|
||||
resourcesDecoder.resXmlSerializer
|
||||
).decodeManifest(
|
||||
apkFile.directory.getFileInput("AndroidManifest.xml"),
|
||||
// Older Android versions do not support OutputStream.nullOutputStream()
|
||||
object : OutputStream() {
|
||||
override fun write(b: Int) { /* do nothing */
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
|
||||
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
|
||||
context.packageMetadata.let { metadata ->
|
||||
metadata.packageName = resourcesDecoder.resTable.packageRenamed
|
||||
versionInfo.let {
|
||||
metadata.packageVersion = it.versionName ?: it.versionCode
|
||||
}
|
||||
|
||||
/*
|
||||
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
|
||||
because ResourcesDecoder.decodeResources loads the main package
|
||||
and not XmlPullStreamDecoder.decodeManifest.
|
||||
See ARSCDecoder.readTableType for more info.
|
||||
|
||||
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
|
||||
*/
|
||||
metadata.apkInfo.sparseResources = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
operator fun get(path: String) = options.resourceCachePath.resolve(path)
|
||||
|
||||
override fun iterator() = options.resourceCachePath.walkTopDown().iterator()
|
||||
|
||||
|
||||
/**
|
||||
* Compile resources from the [ResourceContext].
|
||||
*
|
||||
* @return The compiled resources.
|
||||
*/
|
||||
override fun get(): File? {
|
||||
var resourceFile: File? = null
|
||||
|
||||
if (options.resourceDecodingMode == ResourceDecodingMode.FULL) {
|
||||
logger.info("Compiling modified resources")
|
||||
|
||||
val cacheDirectory = ExtFile(options.resourceCachePath)
|
||||
val aaptFile = cacheDirectory.resolve("aapt_temp_file").also {
|
||||
Files.deleteIfExists(it.toPath())
|
||||
}.also { resourceFile = it }
|
||||
|
||||
try {
|
||||
AaptInvoker(
|
||||
options.resourceConfig, context.packageMetadata.apkInfo
|
||||
).invokeAapt(aaptFile,
|
||||
cacheDirectory.resolve("AndroidManifest.xml").also {
|
||||
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
|
||||
},
|
||||
cacheDirectory.resolve("res"),
|
||||
null,
|
||||
null,
|
||||
context.packageMetadata.apkInfo.usesFramework.let { usesFramework ->
|
||||
usesFramework.ids.map { id ->
|
||||
Framework(options.resourceConfig).getFrameworkApk(id, usesFramework.tag)
|
||||
}.toTypedArray()
|
||||
})
|
||||
} finally {
|
||||
cacheDirectory.close()
|
||||
}
|
||||
}
|
||||
|
||||
return resourceFile
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of decoding the resources.
|
||||
*/
|
||||
internal enum class ResourceDecodingMode {
|
||||
/**
|
||||
* Decode all resources.
|
||||
*/
|
||||
FULL,
|
||||
|
||||
/**
|
||||
* Decode the manifest file only.
|
||||
*/
|
||||
MANIFEST_ONLY,
|
||||
}
|
||||
|
||||
inner class XmlFileHolder {
|
||||
operator fun get(inputStream: InputStream) =
|
||||
DomFileEditor(inputStream)
|
||||
|
||||
operator fun get(path: String): DomFileEditor {
|
||||
return DomFileEditor(this@ResourceContext[path])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -22,8 +22,7 @@ object InstructionExtensions {
|
||||
fun MutableMethodImplementation.addInstructions(
|
||||
index: Int,
|
||||
instructions: List<BuilderInstruction>
|
||||
) =
|
||||
instructions.asReversed().forEach { addInstruction(index, it) }
|
||||
) = instructions.asReversed().forEach { addInstruction(index, it) }
|
||||
|
||||
/**
|
||||
* Add instructions to a method.
|
||||
|
@@ -1,17 +1,11 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||
|
||||
object MethodFingerprintExtensions {
|
||||
|
||||
/**
|
||||
* The name of a [MethodFingerprint].
|
||||
*/
|
||||
val MethodFingerprint.name: String
|
||||
get() = this.javaClass.simpleName
|
||||
|
||||
// TODO: Make this a property.
|
||||
/**
|
||||
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
|
||||
*/
|
||||
|
@@ -1,72 +0,0 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import app.revanced.patcher.annotation.Compatibility
|
||||
import app.revanced.patcher.annotation.Description
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.annotation.Version
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.patch.OptionsContainer
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchOptions
|
||||
import app.revanced.patcher.patch.annotations.DependsOn
|
||||
import app.revanced.patcher.patch.annotations.RequiresIntegrations
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.companionObject
|
||||
import kotlin.reflect.full.companionObjectInstance
|
||||
|
||||
object PatchExtensions {
|
||||
/**
|
||||
* The name of a [Patch].
|
||||
*/
|
||||
val Class<out Patch<Context>>.patchName: String
|
||||
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
|
||||
|
||||
/**
|
||||
* The version of a [Patch].
|
||||
*/
|
||||
@Deprecated("This property is deprecated and will be removed in a future release.")
|
||||
val Class<out Patch<Context>>.version
|
||||
get() = findAnnotationRecursively(Version::class)?.version
|
||||
|
||||
/**
|
||||
* Weather or not a [Patch] should be included.
|
||||
*/
|
||||
val Class<out Patch<Context>>.include
|
||||
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
||||
|
||||
/**
|
||||
* The description of a [Patch].
|
||||
*/
|
||||
val Class<out Patch<Context>>.description
|
||||
get() = findAnnotationRecursively(Description::class)?.description
|
||||
|
||||
/**
|
||||
* The dependencies of a [Patch].
|
||||
*/
|
||||
val Class<out Patch<Context>>.dependencies
|
||||
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
|
||||
|
||||
/**
|
||||
* The packages a [Patch] is compatible with.
|
||||
*/
|
||||
val Class<out Patch<Context>>.compatiblePackages
|
||||
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
|
||||
|
||||
/**
|
||||
* Weather or not a [Patch] requires integrations.
|
||||
*/
|
||||
internal val Class<out Patch<Context>>.requiresIntegrations
|
||||
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
|
||||
|
||||
/**
|
||||
* The options of a [Patch].
|
||||
*/
|
||||
val Class<out Patch<Context>>.options: PatchOptions?
|
||||
get() = kotlin.companionObject?.let { cl ->
|
||||
if (cl.visibility != KVisibility.PUBLIC) return null
|
||||
kotlin.companionObjectInstance?.let {
|
||||
(it as? OptionsContainer)?.options
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
package app.revanced.patcher.fingerprint
|
||||
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
|
||||
/**
|
||||
* A ReVanced fingerprint.
|
||||
* Can be a [MethodFingerprint].
|
||||
*/
|
||||
interface Fingerprint
|
125
src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt
Normal file
125
src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt
Normal file
@@ -0,0 +1,125 @@
|
||||
package app.revanced.patcher.fingerprint
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
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 java.util.*
|
||||
|
||||
internal typealias MethodClassPair = Pair<Method, ClassDef>
|
||||
|
||||
/**
|
||||
* Lookup map for methods.
|
||||
*/
|
||||
internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by mutableMapOf() {
|
||||
/**
|
||||
* Adds a [MethodClassPair] to the list associated with the given key.
|
||||
* If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
|
||||
*/
|
||||
fun add(
|
||||
key: String,
|
||||
methodClassPair: MethodClassPair
|
||||
) {
|
||||
getOrPut(key) { MethodClassList() }.add(methodClassPair)
|
||||
}
|
||||
|
||||
/**
|
||||
* List of methods and the class they are a member of.
|
||||
*/
|
||||
internal class MethodClassList : LinkedList<MethodClassPair>()
|
||||
|
||||
companion object Maps {
|
||||
/**
|
||||
* A list of methods and the class they are a member of.
|
||||
*/
|
||||
internal val methods = MethodClassList()
|
||||
|
||||
/**
|
||||
* Lookup map for methods keyed to the methods access flags, return type and parameter.
|
||||
*/
|
||||
internal val methodSignatureLookupMap = LookupMap()
|
||||
|
||||
/**
|
||||
* Lookup map for methods associated by strings referenced in the method.
|
||||
*/
|
||||
internal val methodStringsLookupMap = LookupMap()
|
||||
|
||||
/**
|
||||
* Initializes lookup maps for [MethodFingerprint] resolution
|
||||
* using attributes of methods such as the method signature or strings.
|
||||
*
|
||||
* @param context The [BytecodeContext] containing the classes to initialize the lookup maps with.
|
||||
*/
|
||||
internal fun initializeLookupMaps(context: BytecodeContext) {
|
||||
if (methods.isNotEmpty()) clearLookupMaps()
|
||||
|
||||
context.classes.forEach { classDef ->
|
||||
classDef.methods.forEach { method ->
|
||||
val methodClassPair = method to classDef
|
||||
|
||||
// For fingerprints with no access or return type specified.
|
||||
methods += methodClassPair
|
||||
|
||||
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
|
||||
|
||||
// Add <access><returnType> as the key.
|
||||
methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair)
|
||||
|
||||
// Add <access><returnType>[parameters] as the key.
|
||||
methodSignatureLookupMap.add(
|
||||
buildString {
|
||||
append(accessFlagsReturnKey)
|
||||
appendParameters(method.parameterTypes)
|
||||
},
|
||||
methodClassPair
|
||||
)
|
||||
|
||||
// Add strings contained in the method as the key.
|
||||
method.implementation?.instructions?.forEach instructions@{ instruction ->
|
||||
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO)
|
||||
return@instructions
|
||||
|
||||
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
|
||||
|
||||
methodStringsLookupMap.add(string, methodClassPair)
|
||||
}
|
||||
|
||||
// In the future, the class type could be added to the lookup map.
|
||||
// This would require MethodFingerprint to be changed to include the class type.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the internal lookup maps created in [initializeLookupMaps]
|
||||
*/
|
||||
internal fun clearLookupMaps() {
|
||||
methods.clear()
|
||||
methodSignatureLookupMap.clear()
|
||||
methodStringsLookupMap.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a string based on the parameter reference types of this method.
|
||||
*/
|
||||
internal fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
|
||||
// Maximum parameters to use in the signature key.
|
||||
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
|
||||
// To keep the signature map from becoming needlessly bloated,
|
||||
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
|
||||
// The value of 5 was chosen based on local performance testing and is not set in stone.
|
||||
val maxSignatureParameters = 5
|
||||
// Must append a unique value before the parameters to distinguish this key includes the parameters.
|
||||
// If this is not appended, then methods with no parameters
|
||||
// will collide with different keys that specify access/return but omit the parameters.
|
||||
append("p:")
|
||||
parameters.forEachIndexed { index, parameter ->
|
||||
if (index >= maxSignatureParameters) return
|
||||
append(parameter.first())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
package app.revanced.patcher.fingerprint
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
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.util.MethodUtil
|
||||
|
||||
/**
|
||||
* Represents the result of a [MethodFingerprintResult].
|
||||
*
|
||||
* @param method The matching method.
|
||||
* @param classDef The [ClassDef] that contains the matching [method].
|
||||
* @param scanResult The result of scanning for the [MethodFingerprint].
|
||||
* @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class MethodFingerprintResult(
|
||||
val method: Method,
|
||||
val classDef: ClassDef,
|
||||
val scanResult: MethodFingerprintScanResult,
|
||||
internal val context: BytecodeContext
|
||||
) {
|
||||
/**
|
||||
* Returns a mutable clone of [classDef]
|
||||
*
|
||||
* Please note, this method allocates a [ClassProxy].
|
||||
* Use [classDef] where possible.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
||||
|
||||
/**
|
||||
* Returns a mutable clone of [method]
|
||||
*
|
||||
* Please note, this method allocates a [ClassProxy].
|
||||
* Use [method] where possible.
|
||||
*/
|
||||
val mutableMethod by lazy {
|
||||
mutableClass.methods.first {
|
||||
MethodUtil.methodSignaturesMatch(it, this.method)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of scanning on the [MethodFingerprint].
|
||||
* @param patternScanResult The result of the pattern scan.
|
||||
* @param stringsScanResult The result of the string scan.
|
||||
*/
|
||||
class MethodFingerprintScanResult(
|
||||
val patternScanResult: PatternScanResult?,
|
||||
val stringsScanResult: StringsScanResult?
|
||||
) {
|
||||
/**
|
||||
* The result of scanning strings on the [MethodFingerprint].
|
||||
* @param matches The list of strings that were matched.
|
||||
*/
|
||||
class StringsScanResult(val matches: List<StringMatch>) {
|
||||
/**
|
||||
* Represents a match for a string at an index.
|
||||
* @param string The string that was matched.
|
||||
* @param index The index of the string.
|
||||
*/
|
||||
class StringMatch(val string: String, val index: Int)
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a pattern scan.
|
||||
* @param startIndex The start index of the instructions where to which this pattern matches.
|
||||
* @param endIndex The end index of the instructions where to which this pattern matches.
|
||||
* @param warnings A list of warnings considering this [PatternScanResult].
|
||||
*/
|
||||
class PatternScanResult(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int,
|
||||
var warnings: List<Warning>? = null
|
||||
) {
|
||||
/**
|
||||
* Represents warnings of the pattern scan.
|
||||
* @param correctOpcode The opcode the instruction list has.
|
||||
* @param wrongOpcode The opcode the pattern list of the signature currently has.
|
||||
* @param instructionIndex The index of the opcode relative to the instruction list.
|
||||
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
|
||||
*/
|
||||
class Warning(
|
||||
val correctOpcode: Opcode,
|
||||
val wrongOpcode: Opcode,
|
||||
val instructionIndex: Int,
|
||||
val patternIndex: Int,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
package app.revanced.patcher.fingerprint.method.annotation
|
||||
package app.revanced.patcher.fingerprint.annotation
|
||||
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||
|
||||
/**
|
||||
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user