You've already forked revanced-patcher
mirror of
https://github.com/revanced/revanced-patcher
synced 2025-09-10 05:30:49 +02:00
Compare commits
174 Commits
v1.0.0-dev
...
v1.0.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1ce6098cad | ||
![]() |
46a6396114 | ||
![]() |
41e88605c3 | ||
![]() |
893d4c699b | ||
![]() |
bf8655ead8 | ||
![]() |
6c65952d80 | ||
![]() |
26f3e7336b | ||
![]() |
849616dc2b | ||
![]() |
e8a131fb08 | ||
![]() |
11abc67d9a | ||
![]() |
8615798711 | ||
![]() |
070e02ea28 | ||
![]() |
54511a4fc6 | ||
![]() |
94f3c9116e | ||
![]() |
fe56c0cadb | ||
![]() |
bf4894592b | ||
![]() |
5a96f2d99f | ||
![]() |
839a5ef22a | ||
![]() |
0ebdb100fd | ||
![]() |
4f60bea81e | ||
![]() |
1a49d9439f | ||
![]() |
5535eb4f01 | ||
![]() |
5dc6aa9bfd | ||
![]() |
6c0d28191b | ||
![]() |
9d067b9b0d | ||
![]() |
a4d8be20fc | ||
![]() |
ec9fd15f9b | ||
![]() |
d20f7fd6e1 | ||
![]() |
ebbcf78e56 | ||
![]() |
024fa867e1 | ||
![]() |
e506d8fd8c | ||
![]() |
f5b5c52e55 | ||
![]() |
fc05fe79de | ||
![]() |
79909cf260 | ||
![]() |
8f12873200 | ||
![]() |
cc9416dd11 | ||
![]() |
99319e63da | ||
![]() |
c459beb5f8 | ||
![]() |
e5ae970009 | ||
![]() |
612515acf8 | ||
![]() |
b1eebc99a7 | ||
![]() |
6cb7cdb0b2 | ||
![]() |
beff1df9b0 | ||
![]() |
f58a498849 | ||
![]() |
6cb1fdf617 | ||
![]() |
7399450139 | ||
![]() |
193eae298a | ||
![]() |
4e56652429 | ||
![]() |
7755bbc645 | ||
![]() |
f3b5f67b39 | ||
![]() |
7f18bbf66e | ||
![]() |
c8b68e36e0 | ||
![]() |
5ca5a1c29e | ||
![]() |
d677d9e800 | ||
![]() |
e5bea06353 | ||
![]() |
8f1a629191 | ||
![]() |
e6086511e5 | ||
![]() |
c612676543 | ||
![]() |
bca948658e | ||
![]() |
0e5f4ba2d5 | ||
![]() |
6ca05769ef | ||
![]() |
c21e5affba | ||
![]() |
1c5a04caf9 | ||
![]() |
3d6a1d38f3 | ||
![]() |
715a2ad025 | ||
![]() |
9889ec9d03 | ||
![]() |
01bfbd656e | ||
![]() |
6c9797583d | ||
![]() |
3cf07f5ce2 | ||
![]() |
0bfb92a0cb | ||
![]() |
042638a399 | ||
![]() |
4178a1eedc | ||
![]() |
338bd9f739 | ||
![]() |
c55c62a57e | ||
![]() |
1f08da8b2a | ||
![]() |
b2dab3fabf | ||
![]() |
0f30eac32c | ||
![]() |
642e9031eb | ||
![]() |
7a56dca004 | ||
![]() |
18853f70a4 | ||
![]() |
6b8b0573d4 | ||
![]() |
3889d72927 | ||
![]() |
3f97cc8e1f | ||
![]() |
1db735b1e2 | ||
![]() |
996c4acb20 | ||
![]() |
5b28523eea | ||
![]() |
72f3cad3f9 | ||
![]() |
9659a61c5c | ||
![]() |
b892729332 | ||
![]() |
6e4db110c8 | ||
![]() |
e810197e2a | ||
![]() |
e65ebd27c2 | ||
![]() |
5bd416b409 | ||
![]() |
a6c6b4979a | ||
![]() |
db10ab03be | ||
![]() |
94dbb573cf | ||
![]() |
08253ee010 | ||
![]() |
3144ec872a | ||
![]() |
48c4ea2f6d | ||
![]() |
c63b20fa65 | ||
![]() |
f806cb38c5 | ||
![]() |
6c3e2d79ea | ||
![]() |
cca12aa34a | ||
![]() |
c267b12a7d | ||
![]() |
eef448cc39 | ||
![]() |
45303f66ec | ||
![]() |
783fbf43f2 | ||
![]() |
59189058ac | ||
![]() |
dfac8f03a3 | ||
![]() |
b44bf4c267 | ||
![]() |
066ad274ed | ||
![]() |
a8653fe6a0 | ||
![]() |
139a23b750 | ||
![]() |
0d8d19e708 | ||
![]() |
a1e909b163 | ||
![]() |
e4157332d3 | ||
![]() |
923efc4caf | ||
![]() |
bea0cbc550 | ||
![]() |
1ee2e4ba56 | ||
![]() |
fac44a50c3 | ||
![]() |
544bcf76bd | ||
![]() |
a16c8cabf2 | ||
![]() |
2777117da2 | ||
![]() |
66a9b76845 | ||
![]() |
f068fc87ff | ||
![]() |
7cc8a7dec3 | ||
![]() |
398239dc10 | ||
![]() |
d18a3b6a28 | ||
![]() |
bfe4e3e298 | ||
![]() |
a1b6b06bd3 | ||
![]() |
4087f49863 | ||
![]() |
00c85b5d75 | ||
![]() |
cb78c5a86f | ||
![]() |
9991f39c9a | ||
![]() |
650bf71124 | ||
![]() |
1dd3394ea3 | ||
![]() |
e6c2501539 | ||
![]() |
4b26305bd5 | ||
![]() |
ce21bd60f3 | ||
![]() |
3651981161 | ||
![]() |
6299b9e951 | ||
![]() |
24b544708f | ||
![]() |
c7ef2644d8 | ||
![]() |
fa6e454ae9 | ||
![]() |
cbd8df2df0 | ||
![]() |
f3d8b917de | ||
![]() |
82c530650f | ||
![]() |
81d0cf20f3 | ||
![]() |
36d4f71325 | ||
![]() |
699c730a7c | ||
![]() |
b197956e39 | ||
![]() |
be992a3e6f | ||
![]() |
e0437397df | ||
![]() |
41749ba829 | ||
![]() |
8161ce4fa6 | ||
![]() |
6626014ef3 | ||
![]() |
e6e468fbb5 | ||
![]() |
2fabbdf71b | ||
![]() |
7faa001406 | ||
![]() |
70872307e3 | ||
![]() |
310a7c446b | ||
![]() |
c236ebe078 | ||
![]() |
674461f08d | ||
![]() |
bbb2c547aa | ||
![]() |
b957501e70 | ||
![]() |
b6ca31a970 | ||
![]() |
cbcf93f7d6 | ||
![]() |
d5b4c99c00 | ||
![]() |
4dd820ffdf | ||
![]() |
cb9b1b9416 | ||
![]() |
428f7f4dec | ||
![]() |
77536cce8f | ||
![]() |
7b6a61b674 | ||
![]() |
7f5f3b217d |
42
.github/workflows/release.yml
vendored
Normal file
42
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup JDK
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt'
|
||||||
|
cache: gradle
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
- name: Make gradlew executable
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Build with Gradle
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew build
|
||||||
|
- name: Setup semantic-release
|
||||||
|
run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog gradle-semantic-release-plugin -D
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: npx semantic-release
|
15
.idea/git_toolbox_prj.xml
generated
Normal file
15
.idea/git_toolbox_prj.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitToolBoxProjectSettings">
|
||||||
|
<option name="commitMessageIssueKeyValidationOverride">
|
||||||
|
<BoolValueOverride>
|
||||||
|
<option name="enabled" value="true" />
|
||||||
|
</BoolValueOverride>
|
||||||
|
</option>
|
||||||
|
<option name="commitMessageValidationEnabledOverride">
|
||||||
|
<BoolValueOverride>
|
||||||
|
<option name="enabled" value="true" />
|
||||||
|
</BoolValueOverride>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="UnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/vcs.xml
generated
6
.idea/vcs.xml
generated
@@ -1,5 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="CommitMessageInspectionProfile">
|
||||||
|
<profile version="1.0">
|
||||||
|
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
|
25
.releaserc
Normal file
25
.releaserc
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"branches": [
|
||||||
|
"main",
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"prerelease": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
"gradle-semantic-release-plugin",
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"CHANGELOG.md",
|
||||||
|
"gradle.properties"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/github"
|
||||||
|
]
|
||||||
|
}
|
377
CHANGELOG.md
Normal file
377
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,41 +1,69 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.6.10"
|
kotlin("jvm") version "1.6.21"
|
||||||
|
java
|
||||||
`maven-publish`
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.revanced"
|
group = "app.revanced"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven {
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||||
|
credentials {
|
||||||
|
// DO NOT set these variables in the project's gradle.properties.
|
||||||
|
// Instead, you should set them in:
|
||||||
|
// Windows: %homepath%\.gradle\gradle.properties
|
||||||
|
// Linux: ~/.gradle/gradle.properties
|
||||||
|
username =
|
||||||
|
project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") // DO NOT CHANGE!
|
||||||
|
password =
|
||||||
|
project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") // DO NOT CHANGE!
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib"))
|
implementation(kotlin("stdlib"))
|
||||||
implementation("org.ow2.asm:asm:9.2")
|
|
||||||
implementation("org.ow2.asm:asm-util:9.2")
|
api("xpp3:xpp3:1.1.4c")
|
||||||
implementation("org.ow2.asm:asm-tree:9.2")
|
api("org.apktool:apktool-lib:2.6.1")
|
||||||
implementation("org.ow2.asm:asm-commons:9.2")
|
api("app.revanced:multidexlib2:2.5.2.r2")
|
||||||
implementation("io.github.microutils:kotlin-logging:2.1.21")
|
api("org.smali:smali:2.5.2")
|
||||||
testImplementation("ch.qos.logback:logback-classic:1.2.11") // use your own logger!
|
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks {
|
||||||
useJUnitPlatform()
|
test {
|
||||||
testLogging {
|
useJUnitPlatform()
|
||||||
events("PASSED", "SKIPPED", "FAILED")
|
testLogging {
|
||||||
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
withJavadocJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
val isGitHubCI = System.getenv("GITHUB_ACTOR") != null
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
if (isGitHubCI) {
|
||||||
name = "GitHubPackages"
|
maven {
|
||||||
url = uri("https://maven.pkg.github.com/ReVancedTeam/revanced-patcher")
|
name = "GitHubPackages"
|
||||||
credentials {
|
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
credentials {
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
username = System.getenv("GITHUB_ACTOR")
|
||||||
|
password = System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
mavenLocal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
publications {
|
publications {
|
||||||
@@ -43,4 +71,4 @@ publishing {
|
|||||||
from(components["java"])
|
from(components["java"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,2 +1,2 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style = official
|
||||||
version=1.0.0
|
version = 1.0.0
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
244
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
244
src/main/kotlin/app/revanced/patcher/Patcher.kt
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.PatcherData
|
||||||
|
import app.revanced.patcher.data.base.Data
|
||||||
|
import app.revanced.patcher.data.implementation.findIndexed
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
|
import app.revanced.patcher.extensions.nullOutputStream
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import app.revanced.patcher.patch.implementation.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.implementation.ResourcePatch
|
||||||
|
import app.revanced.patcher.patch.implementation.misc.PatchResult
|
||||||
|
import app.revanced.patcher.patch.implementation.misc.PatchResultError
|
||||||
|
import app.revanced.patcher.patch.implementation.misc.PatchResultSuccess
|
||||||
|
import app.revanced.patcher.signature.implementation.method.resolver.MethodSignatureResolver
|
||||||
|
import app.revanced.patcher.util.ListBackedSet
|
||||||
|
import brut.androlib.Androlib
|
||||||
|
import brut.androlib.meta.UsesFramework
|
||||||
|
import brut.androlib.res.AndrolibResources
|
||||||
|
import brut.androlib.res.data.ResPackage
|
||||||
|
import brut.androlib.res.decoder.AXmlResourceParser
|
||||||
|
import brut.androlib.res.decoder.ResAttrDecoder
|
||||||
|
import brut.androlib.res.decoder.XmlPullStreamDecoder
|
||||||
|
import brut.directory.ExtFile
|
||||||
|
import lanchon.multidexlib2.BasicDexFileNamer
|
||||||
|
import lanchon.multidexlib2.DexIO
|
||||||
|
import lanchon.multidexlib2.MultiDexIO
|
||||||
|
import org.jf.dexlib2.Opcodes
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.iface.DexFile
|
||||||
|
import org.jf.dexlib2.writer.io.MemoryDataStore
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
val NAMER = BasicDexFileNamer()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ReVanced Patcher.
|
||||||
|
* @param options The options for the patcher.
|
||||||
|
*/
|
||||||
|
class Patcher(
|
||||||
|
private val options: PatcherOptions
|
||||||
|
) {
|
||||||
|
val packageVersion: String
|
||||||
|
val packageName: String
|
||||||
|
|
||||||
|
private lateinit var usesFramework: UsesFramework
|
||||||
|
private val patcherData: PatcherData
|
||||||
|
private val opcodes: Opcodes
|
||||||
|
|
||||||
|
init {
|
||||||
|
val extFileInput = ExtFile(options.inputFile)
|
||||||
|
val outDir = File(options.resourceCacheDirectory)
|
||||||
|
|
||||||
|
if (outDir.exists()) outDir.deleteRecursively()
|
||||||
|
outDir.mkdir()
|
||||||
|
|
||||||
|
// load the resource table from the input file
|
||||||
|
val androlib = Androlib()
|
||||||
|
val resourceTable = androlib.getResTable(extFileInput, true)
|
||||||
|
|
||||||
|
if (options.patchResources) {
|
||||||
|
// 1. decode resources to cache directory
|
||||||
|
androlib.decodeManifestWithResources(extFileInput, outDir, resourceTable)
|
||||||
|
androlib.decodeResourcesFull(extFileInput, outDir, resourceTable)
|
||||||
|
|
||||||
|
// 2. read framework ids from the resource table
|
||||||
|
usesFramework = UsesFramework()
|
||||||
|
usesFramework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
|
||||||
|
} else {
|
||||||
|
// create decoder for the resource table
|
||||||
|
val decoder = ResAttrDecoder()
|
||||||
|
decoder.currentPackage = ResPackage(resourceTable, 0, null)
|
||||||
|
|
||||||
|
// create xml parser with the decoder
|
||||||
|
val axmlParser = AXmlResourceParser()
|
||||||
|
axmlParser.attrDecoder = decoder
|
||||||
|
|
||||||
|
// parse package information with the decoder and parser which will set required values in the resource table
|
||||||
|
// instead of decodeManifest another more low level solution can be created to make it faster/better
|
||||||
|
XmlPullStreamDecoder(
|
||||||
|
axmlParser, AndrolibResources().resXmlSerializer
|
||||||
|
).decodeManifest(
|
||||||
|
extFileInput.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set package information
|
||||||
|
packageVersion = resourceTable.versionInfo.versionName
|
||||||
|
packageName = resourceTable.currentResPackage.name
|
||||||
|
// read dex files
|
||||||
|
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
||||||
|
opcodes = dexFile.opcodes
|
||||||
|
|
||||||
|
// save to patcher data
|
||||||
|
patcherData = PatcherData(dexFile.classes.toMutableList(), options.resourceCacheDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add additional dex file container to the patcher.
|
||||||
|
* @param files The dex file containers to add to the patcher.
|
||||||
|
* @param allowedOverwrites A list of class types that are allowed to be overwritten.
|
||||||
|
* @param throwOnDuplicates If this is set to true, the patcher will throw an exception if a duplicate class has been found.
|
||||||
|
*/
|
||||||
|
fun addFiles(
|
||||||
|
files: Iterable<File>, allowedOverwrites: Iterable<String> = emptyList(), throwOnDuplicates: Boolean = false
|
||||||
|
) {
|
||||||
|
for (file in files) {
|
||||||
|
val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null)
|
||||||
|
for (classDef in dexFile.classes) {
|
||||||
|
val e = patcherData.bytecodeData.classes.internalClasses.findIndexed { it.type == classDef.type }
|
||||||
|
if (e != null) {
|
||||||
|
if (throwOnDuplicates) {
|
||||||
|
throw Exception("Class ${classDef.type} has already been added to the patcher.")
|
||||||
|
}
|
||||||
|
val (_, idx) = e
|
||||||
|
if (allowedOverwrites.contains(classDef.type)) {
|
||||||
|
patcherData.bytecodeData.classes.internalClasses[idx] = classDef
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
patcherData.bytecodeData.classes.internalClasses.add(classDef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the patched dex file.
|
||||||
|
*/
|
||||||
|
fun save(): Map<String, MemoryDataStore> {
|
||||||
|
val newDexFile = object : DexFile {
|
||||||
|
override fun getClasses(): Set<ClassDef> {
|
||||||
|
patcherData.bytecodeData.classes.applyProxies()
|
||||||
|
return ListBackedSet(patcherData.bytecodeData.classes.internalClasses)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getOpcodes(): Opcodes {
|
||||||
|
return this@Patcher.opcodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build modified resources
|
||||||
|
if (options.patchResources) {
|
||||||
|
val extDir = ExtFile(options.resourceCacheDirectory)
|
||||||
|
|
||||||
|
// TODO: figure out why a new instance of Androlib is necessary here
|
||||||
|
Androlib().buildResources(extDir, usesFramework)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write dex modified files
|
||||||
|
val output = mutableMapOf<String, MemoryDataStore>()
|
||||||
|
MultiDexIO.writeDexFile(
|
||||||
|
true, -1, // core count
|
||||||
|
output, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
|
||||||
|
)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add [Patch]es to the patcher.
|
||||||
|
* @param patches [Patch]es The patches to add.
|
||||||
|
*/
|
||||||
|
fun addPatches(patches: Iterable<Class<out Patch<Data>>>) {
|
||||||
|
patcherData.patches.addAll(patches)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a [patch] and its dependencies recursively.
|
||||||
|
* @param patch The [patch] to apply.
|
||||||
|
* @param appliedPatches A list of [patch] names, to prevent applying [patch]es twice.
|
||||||
|
* @return The result of executing the [patch].
|
||||||
|
*/
|
||||||
|
private fun applyPatch(
|
||||||
|
patch: Class<out Patch<Data>>, appliedPatches: MutableList<String>
|
||||||
|
): PatchResult {
|
||||||
|
val patchName = patch.patchName
|
||||||
|
|
||||||
|
// if the patch has already applied silently skip it
|
||||||
|
if (appliedPatches.contains(patchName)) return PatchResultSuccess()
|
||||||
|
appliedPatches.add(patchName)
|
||||||
|
|
||||||
|
// recursively apply all dependency patches
|
||||||
|
patch.dependencies?.forEach {
|
||||||
|
val patchDependency = it.java
|
||||||
|
|
||||||
|
val result = applyPatch(patchDependency, appliedPatches)
|
||||||
|
if (result.isSuccess()) return@forEach
|
||||||
|
|
||||||
|
val errorMessage = result.error()!!.message
|
||||||
|
return PatchResultError("$patchName depends on ${patchDependency.patchName} but the following error was raised: $errorMessage")
|
||||||
|
}
|
||||||
|
|
||||||
|
val patchInstance = patch.getDeclaredConstructor().newInstance()
|
||||||
|
|
||||||
|
// if the current patch is a resource patch but resource patching is disabled, return an error
|
||||||
|
val isResourcePatch = patchInstance is ResourcePatch
|
||||||
|
if (!options.patchResources && isResourcePatch) return PatchResultError("$patchName is a resource patch, but resource patching is disabled.")
|
||||||
|
|
||||||
|
// TODO: find a solution for this
|
||||||
|
val data = if (isResourcePatch) {
|
||||||
|
patcherData.resourceData
|
||||||
|
} else {
|
||||||
|
MethodSignatureResolver(
|
||||||
|
patcherData.bytecodeData.classes.internalClasses, (patchInstance as BytecodePatch).signatures
|
||||||
|
).resolve(patcherData)
|
||||||
|
patcherData.bytecodeData
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
patchInstance.execute(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
PatchResultError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply patches loaded into the patcher.
|
||||||
|
* @param stopOnError If true, the patches will stop on the first error.
|
||||||
|
* @return A map of [PatchResultSuccess]. If the [Patch] was successfully applied,
|
||||||
|
* [PatchResultSuccess] will always be returned to the wrapping Result object.
|
||||||
|
* If the [Patch] failed to apply, an Exception will always be returned to the wrapping Result object.
|
||||||
|
*/
|
||||||
|
fun applyPatches(
|
||||||
|
stopOnError: Boolean = false, callback: (String) -> Unit = {}
|
||||||
|
): Map<String, Result<PatchResultSuccess>> {
|
||||||
|
val appliedPatches = mutableListOf<String>()
|
||||||
|
|
||||||
|
return buildMap {
|
||||||
|
for (patch in patcherData.patches) {
|
||||||
|
val result = applyPatch(patch, appliedPatches)
|
||||||
|
|
||||||
|
val name = patch.patchName
|
||||||
|
callback(name)
|
||||||
|
|
||||||
|
this[name] = if (result.isSuccess()) {
|
||||||
|
Result.success(result.success()!!)
|
||||||
|
} else {
|
||||||
|
Result.failure(result.error()!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopOnError && result.isError()) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
src/main/kotlin/app/revanced/patcher/PatcherOptions.kt
Normal file
15
src/main/kotlin/app/revanced/patcher/PatcherOptions.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param inputFile The input file (usually an apk file).
|
||||||
|
* @param resourceCacheDirectory Directory to cache resources.
|
||||||
|
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.
|
||||||
|
*/
|
||||||
|
data class PatcherOptions(
|
||||||
|
internal val inputFile: File,
|
||||||
|
// TODO: maybe a file system in memory is better. Could cause high memory usage.
|
||||||
|
internal val resourceCacheDirectory: String,
|
||||||
|
internal val patchResources: Boolean = false
|
||||||
|
)
|
@@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.patcher.annotation
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to constrain a [Patch] or [MethodSignature] to compatible packages.
|
||||||
|
* @param compatiblePackages A list of packages a [Patch] or [MethodSignature] is compatible with.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
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] or [MethodSignature]is compatible with.
|
||||||
|
*/
|
||||||
|
@Target()
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class Package(
|
||||||
|
val name: String,
|
||||||
|
val versions: Array<String>
|
||||||
|
)
|
@@ -0,0 +1,38 @@
|
|||||||
|
package app.revanced.patcher.annotation
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to name a [Patch] or [MethodSignature].
|
||||||
|
* @param name A suggestive name for the [Patch] or [MethodSignature].
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class Name(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to describe a [Patch] or [MethodSignature].
|
||||||
|
* @param description A description for the [Patch] or [MethodSignature].
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class Description(
|
||||||
|
val description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to version a [Patch] or [MethodSignature].
|
||||||
|
* @param version The version of a [Patch] or [MethodSignature].
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class Version(
|
||||||
|
val version: String,
|
||||||
|
)
|
18
src/main/kotlin/app/revanced/patcher/data/PatcherData.kt
Normal file
18
src/main/kotlin/app/revanced/patcher/data/PatcherData.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.patcher.data
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.base.Data
|
||||||
|
import app.revanced.patcher.data.implementation.BytecodeData
|
||||||
|
import app.revanced.patcher.data.implementation.ResourceData
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
internal data class PatcherData(
|
||||||
|
val internalClasses: MutableList<ClassDef>,
|
||||||
|
val resourceCacheDirectory: String
|
||||||
|
) {
|
||||||
|
internal val patches = mutableListOf<Class<out Patch<Data>>>()
|
||||||
|
|
||||||
|
internal val bytecodeData = BytecodeData(internalClasses)
|
||||||
|
internal val resourceData = ResourceData(File(resourceCacheDirectory))
|
||||||
|
}
|
9
src/main/kotlin/app/revanced/patcher/data/base/Data.kt
Normal file
9
src/main/kotlin/app/revanced/patcher/data/base/Data.kt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patcher.data.base
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.implementation.BytecodeData
|
||||||
|
import app.revanced.patcher.data.implementation.ResourceData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constraint interface for [BytecodeData] and [ResourceData]
|
||||||
|
*/
|
||||||
|
interface Data
|
@@ -0,0 +1,69 @@
|
|||||||
|
package app.revanced.patcher.data.implementation
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.base.Data
|
||||||
|
import app.revanced.patcher.util.ProxyBackedClassList
|
||||||
|
import app.revanced.patcher.util.method.MethodWalker
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
|
||||||
|
class BytecodeData(
|
||||||
|
internalClasses: MutableList<ClassDef>
|
||||||
|
) : Data {
|
||||||
|
val classes = ProxyBackedClassList(internalClasses)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class MethodNotFoundException(s: String) : Exception(s)
|
||||||
|
|
||||||
|
internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
|
||||||
|
for (element in this) {
|
||||||
|
if (predicate(element)) {
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [MethodWalker] instance for the current [BytecodeData].
|
||||||
|
* @param startMethod The method to start at.
|
||||||
|
* @return A [MethodWalker] instance.
|
||||||
|
*/
|
||||||
|
fun BytecodeData.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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BytecodeData.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
|
||||||
|
}
|
@@ -0,0 +1,48 @@
|
|||||||
|
package app.revanced.patcher.data.implementation
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.base.Data
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import javax.xml.XMLConstants
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
import javax.xml.transform.TransformerFactory
|
||||||
|
import javax.xml.transform.dom.DOMSource
|
||||||
|
import javax.xml.transform.stream.StreamResult
|
||||||
|
|
||||||
|
class ResourceData(private val resourceCacheDirectory: File) : Data {
|
||||||
|
private fun resolve(path: String) = resourceCacheDirectory.resolve(path)
|
||||||
|
|
||||||
|
fun forEach(action: (File) -> Unit) = resourceCacheDirectory.walkTopDown().forEach(action)
|
||||||
|
fun get(path: String) = resolve(path)
|
||||||
|
|
||||||
|
fun replace(path: String, oldValue: String, newValue: String, oldValueIsRegex: Boolean = false) {
|
||||||
|
// TODO: buffer this somehow
|
||||||
|
val content = resolve(path).readText()
|
||||||
|
|
||||||
|
if (oldValueIsRegex) {
|
||||||
|
content.replace(Regex(oldValue), newValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getXmlEditor(path: String) = DomFileEditor(resolve(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomFileEditor internal constructor(private val domFile: File) : Closeable {
|
||||||
|
val file: Document
|
||||||
|
|
||||||
|
init {
|
||||||
|
val factory = DocumentBuilderFactory.newInstance()
|
||||||
|
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
|
||||||
|
|
||||||
|
val builder = factory.newDocumentBuilder()
|
||||||
|
|
||||||
|
// this will expectedly throw
|
||||||
|
file = builder.parse(domFile)
|
||||||
|
file.normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = TransformerFactory.newInstance().newTransformer()
|
||||||
|
.transform(DOMSource(file), StreamResult(domFile.outputStream()))
|
||||||
|
}
|
@@ -0,0 +1,57 @@
|
|||||||
|
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.base.Data
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
import app.revanced.patcher.signature.implementation.method.annotation.FuzzyPatternScanMethod
|
||||||
|
import app.revanced.patcher.signature.implementation.method.annotation.MatchingMethod
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively find a given annotation on a class.
|
||||||
|
* @param targetAnnotation The annotation to find.
|
||||||
|
* @return The annotation.
|
||||||
|
*/
|
||||||
|
private fun <T : Annotation> Class<*>.recursiveAnnotation(targetAnnotation: KClass<T>) =
|
||||||
|
this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf())
|
||||||
|
|
||||||
|
|
||||||
|
private fun <T : Annotation> Class<*>.findAnnotationRecursively(
|
||||||
|
targetAnnotation: Class<T>, traversed: MutableSet<Annotation>
|
||||||
|
): T? {
|
||||||
|
val found = this.annotations.firstOrNull { it.annotationClass.java.name == targetAnnotation.name }
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST") if (found != null) return found as T
|
||||||
|
|
||||||
|
for (annotation in this.annotations) {
|
||||||
|
if (traversed.contains(annotation)) continue
|
||||||
|
traversed.add(annotation)
|
||||||
|
|
||||||
|
return (annotation.annotationClass.java.findAnnotationRecursively(targetAnnotation, traversed)) ?: continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
object PatchExtensions {
|
||||||
|
val Class<out Patch<Data>>.patchName: String
|
||||||
|
get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
|
||||||
|
val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version
|
||||||
|
val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description
|
||||||
|
val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Dependencies::class)?.dependencies
|
||||||
|
val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages
|
||||||
|
}
|
||||||
|
|
||||||
|
object MethodSignatureExtensions {
|
||||||
|
val MethodSignature.name: String get() = javaClass.recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
|
||||||
|
val MethodSignature.version get() = javaClass.recursiveAnnotation(Version::class)?.version ?: "0.0.1"
|
||||||
|
val MethodSignature.description get() = javaClass.recursiveAnnotation(Description::class)?.description
|
||||||
|
val MethodSignature.compatiblePackages get() = javaClass.recursiveAnnotation(Compatibility::class)?.compatiblePackages
|
||||||
|
val MethodSignature.matchingMethod get() = javaClass.recursiveAnnotation(MatchingMethod::class)
|
||||||
|
val MethodSignature.fuzzyPatternScanMethod get() = javaClass.recursiveAnnotation(FuzzyPatternScanMethod::class)
|
||||||
|
val MethodSignature.fuzzyThreshold get() = fuzzyPatternScanMethod?.threshold ?: 0
|
||||||
|
}
|
@@ -0,0 +1,92 @@
|
|||||||
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
|
import org.jf.dexlib2.AccessFlags
|
||||||
|
import org.jf.dexlib2.builder.BuilderInstruction
|
||||||
|
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
import org.jf.dexlib2.iface.reference.MethodReference
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableMethod
|
||||||
|
import org.jf.dexlib2.immutable.ImmutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.util.MethodUtil
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value
|
||||||
|
infix fun Int.or(other: AccessFlags) = this or other.value
|
||||||
|
|
||||||
|
fun MutableMethodImplementation.addInstructions(index: Int, instructions: List<BuilderInstruction>) {
|
||||||
|
for (i in instructions.lastIndex downTo 0) {
|
||||||
|
this.addInstruction(index, instructions[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare a method to another, considering constructors and parameters.
|
||||||
|
* @param otherMethod The method to compare against.
|
||||||
|
* @return True if the methods match given the conditions.
|
||||||
|
*/
|
||||||
|
fun Method.softCompareTo(
|
||||||
|
otherMethod: MethodReference
|
||||||
|
): Boolean {
|
||||||
|
if (MethodUtil.isConstructor(this) && !parametersEqual(this.parameterTypes, otherMethod.parameterTypes))
|
||||||
|
return false
|
||||||
|
return this.name == otherMethod.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones the method.
|
||||||
|
* @param registerCount This parameter allows you to change the register count of the method.
|
||||||
|
* This may be a positive or negative number.
|
||||||
|
* @return The **immutable** cloned method. Call [toMutable] or [cloneMutable] to get a **mutable** copy.
|
||||||
|
*/
|
||||||
|
internal fun Method.clone(
|
||||||
|
registerCount: Int = 0,
|
||||||
|
): ImmutableMethod {
|
||||||
|
val clonedImplementation = implementation?.let {
|
||||||
|
ImmutableMethodImplementation(
|
||||||
|
it.registerCount + registerCount,
|
||||||
|
it.instructions,
|
||||||
|
it.tryBlocks,
|
||||||
|
it.debugItems,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ImmutableMethod(
|
||||||
|
returnType,
|
||||||
|
name,
|
||||||
|
parameters,
|
||||||
|
returnType,
|
||||||
|
accessFlags,
|
||||||
|
annotations,
|
||||||
|
hiddenApiRestrictions,
|
||||||
|
clonedImplementation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones the method.
|
||||||
|
* @param registerCount This parameter allows you to change the register count of the method.
|
||||||
|
* This may be a positive or negative number.
|
||||||
|
* @return The **mutable** cloned method. Call [clone] to get an **immutable** copy.
|
||||||
|
*/
|
||||||
|
internal fun Method.cloneMutable(
|
||||||
|
registerCount: Int = 0,
|
||||||
|
) = clone(registerCount).toMutable()
|
||||||
|
|
||||||
|
// FIXME: also check the order of parameters as different order equals different method overload
|
||||||
|
internal fun parametersEqual(
|
||||||
|
parameters1: Iterable<CharSequence>,
|
||||||
|
parameters2: Iterable<CharSequence>
|
||||||
|
): Boolean {
|
||||||
|
return parameters1.count() == parameters2.count() && parameters1.all { parameter ->
|
||||||
|
parameters2.any {
|
||||||
|
it.startsWith(
|
||||||
|
parameter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val nullOutputStream: OutputStream =
|
||||||
|
object : OutputStream() {
|
||||||
|
override fun write(b: Int) {}
|
||||||
|
}
|
@@ -0,0 +1,23 @@
|
|||||||
|
package app.revanced.patcher.patch.annotations
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.base.Data
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation to mark a Class as a patch.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class Patch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation for dependencies of [Patch]es .
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@MustBeDocumented
|
||||||
|
annotation class Dependencies(
|
||||||
|
val dependencies: Array<KClass<out Patch<Data>>> = []
|
||||||
|
)
|
19
src/main/kotlin/app/revanced/patcher/patch/base/Patch.kt
Normal file
19
src/main/kotlin/app/revanced/patcher/patch/base/Patch.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.patcher.patch.base
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.base.Data
|
||||||
|
import app.revanced.patcher.patch.implementation.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.implementation.ResourcePatch
|
||||||
|
import app.revanced.patcher.patch.implementation.misc.PatchResult
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ReVanced patch.
|
||||||
|
* Can either be a [ResourcePatch] or a [BytecodePatch].
|
||||||
|
*/
|
||||||
|
abstract class Patch<out T : Data> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main function of the [Patch] which the patcher will call.
|
||||||
|
*/
|
||||||
|
abstract fun execute(data: @UnsafeVariance T): PatchResult
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.patcher.patch.implementation
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.implementation.BytecodeData
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytecode patch for the Patcher.
|
||||||
|
* @param signatures A list of [MethodSignature] this patch relies on.
|
||||||
|
*/
|
||||||
|
abstract class BytecodePatch(
|
||||||
|
val signatures: Iterable<MethodSignature>
|
||||||
|
) : Patch<BytecodeData>()
|
@@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patcher.patch.implementation
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.implementation.ResourceData
|
||||||
|
import app.revanced.patcher.patch.base.Patch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource patch for the Patcher.
|
||||||
|
*/
|
||||||
|
abstract class ResourcePatch : Patch<ResourceData>()
|
@@ -1,4 +1,4 @@
|
|||||||
package net.revanced.patcher.patch
|
package app.revanced.patcher.patch.implementation.misc
|
||||||
|
|
||||||
interface PatchResult {
|
interface PatchResult {
|
||||||
fun error(): PatchResultError? {
|
fun error(): PatchResultError? {
|
||||||
@@ -24,10 +24,12 @@ interface PatchResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PatchResultError(private val errorMessage: String) : PatchResult {
|
class PatchResultError(
|
||||||
fun errorMessage(): String {
|
errorMessage: String?, cause: Exception?
|
||||||
return errorMessage
|
) : Exception(errorMessage, cause), PatchResult {
|
||||||
}
|
constructor(errorMessage: String) : this(errorMessage, null)
|
||||||
|
constructor(cause: Exception) : this(cause.message, cause)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PatchResultSuccess : PatchResult
|
class PatchResultSuccess : PatchResult
|
@@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.patcher.signature.base
|
||||||
|
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A ReVanced signature.
|
||||||
|
* Can be a [MethodSignature].
|
||||||
|
*/
|
||||||
|
interface Signature
|
@@ -0,0 +1,33 @@
|
|||||||
|
package app.revanced.patcher.signature.implementation.method
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.implementation.MethodNotFoundException
|
||||||
|
import app.revanced.patcher.extensions.MethodSignatureExtensions.name
|
||||||
|
import app.revanced.patcher.signature.base.Signature
|
||||||
|
import app.revanced.patcher.signature.implementation.method.resolver.SignatureResolverResult
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the [MethodSignature] for a method.
|
||||||
|
* @param returnType The return type of the method.
|
||||||
|
* @param accessFlags The access flags of the method.
|
||||||
|
* @param methodParameters The parameters of the method.
|
||||||
|
* @param opcodes The list of opcodes of the method.
|
||||||
|
* @param strings A list of strings which a method contains.
|
||||||
|
* A `null` opcode is equals to an unknown opcode.
|
||||||
|
*/
|
||||||
|
abstract class MethodSignature(
|
||||||
|
internal val returnType: String?,
|
||||||
|
internal val accessFlags: Int?,
|
||||||
|
internal val methodParameters: Iterable<String>?,
|
||||||
|
internal val opcodes: Iterable<Opcode?>?,
|
||||||
|
internal val strings: Iterable<String>? = null
|
||||||
|
) : Signature {
|
||||||
|
/**
|
||||||
|
* The result of the signature
|
||||||
|
*/
|
||||||
|
var result: SignatureResolverResult? = null
|
||||||
|
@Throws(MethodNotFoundException::class)
|
||||||
|
get() {
|
||||||
|
return field ?: throw MethodNotFoundException("Could not resolve required signature ${this.name}")
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
package app.revanced.patcher.signature.implementation.method.annotation
|
||||||
|
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotations for a method which matches to a [MethodSignature].
|
||||||
|
* @param definingClass The defining class name of the method.
|
||||||
|
* @param name A suggestive name for the method which the [MethodSignature] was created for.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class MatchingMethod(
|
||||||
|
val definingClass: String = "L<unspecified-class>;",
|
||||||
|
val name: String = "<unspecified-method>"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotations to scan a pattern [MethodSignature] with fuzzy algorithm.
|
||||||
|
* @param threshold if [threshold] or more of the opcodes do not match, skip.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class FuzzyPatternScanMethod(
|
||||||
|
val threshold: Int = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotations to scan a pattern [MethodSignature] directly.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class DirectPatternScanMethod
|
@@ -0,0 +1,164 @@
|
|||||||
|
package app.revanced.patcher.signature.implementation.method.resolver
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.PatcherData
|
||||||
|
import app.revanced.patcher.data.implementation.proxy
|
||||||
|
import app.revanced.patcher.extensions.MethodSignatureExtensions.fuzzyThreshold
|
||||||
|
import app.revanced.patcher.extensions.parametersEqual
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
import org.jf.dexlib2.iface.instruction.Instruction
|
||||||
|
import org.jf.dexlib2.iface.instruction.formats.Instruction21c
|
||||||
|
import org.jf.dexlib2.iface.reference.StringReference
|
||||||
|
|
||||||
|
internal class MethodSignatureResolver(
|
||||||
|
private val classes: List<ClassDef>,
|
||||||
|
private val methodSignatures: Iterable<MethodSignature>
|
||||||
|
) {
|
||||||
|
fun resolve(patcherData: PatcherData) {
|
||||||
|
for (signature in methodSignatures) {
|
||||||
|
for (classDef in classes) {
|
||||||
|
for (method in classDef.methods) {
|
||||||
|
val patternScanData = compareSignatureToMethod(signature, method) ?: continue
|
||||||
|
|
||||||
|
// create class proxy, in case a patch needs mutability
|
||||||
|
val classProxy = patcherData.bytecodeData.proxy(classDef)
|
||||||
|
signature.result = SignatureResolverResult(
|
||||||
|
classProxy,
|
||||||
|
patternScanData,
|
||||||
|
method,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// These functions do not require the constructor values, so they can be static.
|
||||||
|
companion object {
|
||||||
|
fun resolveFromProxy(
|
||||||
|
classProxy: app.revanced.patcher.util.proxy.ClassProxy,
|
||||||
|
signature: MethodSignature
|
||||||
|
): SignatureResolverResult? {
|
||||||
|
for (method in classProxy.immutableClass.methods) {
|
||||||
|
val result = compareSignatureToMethod(signature, method) ?: continue
|
||||||
|
return SignatureResolverResult(
|
||||||
|
classProxy,
|
||||||
|
result,
|
||||||
|
method,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareSignatureToMethod(
|
||||||
|
signature: MethodSignature,
|
||||||
|
method: Method
|
||||||
|
): PatternScanResult? {
|
||||||
|
signature.returnType?.let {
|
||||||
|
if (!method.returnType.startsWith(signature.returnType)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signature.accessFlags?.let {
|
||||||
|
if (signature.accessFlags != method.accessFlags) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signature.methodParameters?.let {
|
||||||
|
if (!parametersEqual(signature.methodParameters, method.parameterTypes)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signature.strings?.let { strings ->
|
||||||
|
method.implementation ?: return null
|
||||||
|
|
||||||
|
method.implementation!!.instructions.let { instructions ->
|
||||||
|
val stringsList = strings.toMutableList()
|
||||||
|
|
||||||
|
for (instruction in instructions) {
|
||||||
|
if (instruction.opcode != Opcode.CONST_STRING) continue
|
||||||
|
|
||||||
|
val string = ((instruction as Instruction21c).reference as StringReference).string
|
||||||
|
val i = stringsList.indexOfFirst { it == string }
|
||||||
|
if (i != -1) stringsList.removeAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringsList.isNotEmpty()) return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (signature.opcodes == null) {
|
||||||
|
PatternScanResult(0, 0)
|
||||||
|
} else {
|
||||||
|
method.implementation?.instructions?.let {
|
||||||
|
compareOpcodes(signature, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareOpcodes(
|
||||||
|
signature: MethodSignature,
|
||||||
|
instructions: Iterable<Instruction>
|
||||||
|
): PatternScanResult? {
|
||||||
|
val count = instructions.count()
|
||||||
|
val pattern = signature.opcodes!!
|
||||||
|
val size = pattern.count()
|
||||||
|
|
||||||
|
val threshold = signature.fuzzyThreshold
|
||||||
|
|
||||||
|
for (instructionIndex in 0 until count) {
|
||||||
|
var patternIndex = 0
|
||||||
|
var currentThreshold = threshold
|
||||||
|
while (instructionIndex + patternIndex < count) {
|
||||||
|
val originalOpcode = instructions.elementAt(instructionIndex + patternIndex).opcode
|
||||||
|
val patternOpcode = pattern.elementAt(patternIndex)
|
||||||
|
if (
|
||||||
|
patternOpcode != null && // unknown opcode
|
||||||
|
originalOpcode != patternOpcode &&
|
||||||
|
currentThreshold-- == 0
|
||||||
|
) break
|
||||||
|
if (++patternIndex < size) continue
|
||||||
|
patternIndex-- // fix pattern offset
|
||||||
|
|
||||||
|
val result = PatternScanResult(instructionIndex, instructionIndex + patternIndex)
|
||||||
|
|
||||||
|
result.warnings = generateWarnings(signature, instructions, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateWarnings(
|
||||||
|
signature: MethodSignature,
|
||||||
|
instructions: Iterable<Instruction>,
|
||||||
|
scanResult: PatternScanResult,
|
||||||
|
) = buildList {
|
||||||
|
val pattern = signature.opcodes!!
|
||||||
|
for ((patternIndex, instructionIndex) in (scanResult.startIndex until scanResult.endIndex).withIndex()) {
|
||||||
|
val correctOpcode = instructions.elementAt(instructionIndex).opcode
|
||||||
|
val patternOpcode = pattern.elementAt(patternIndex)
|
||||||
|
if (
|
||||||
|
patternOpcode != null && // unknown opcode
|
||||||
|
correctOpcode != patternOpcode
|
||||||
|
) {
|
||||||
|
this.add(
|
||||||
|
PatternScanResult.Warning(
|
||||||
|
correctOpcode, patternOpcode,
|
||||||
|
instructionIndex, patternIndex,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun ClassDef.component1() = this
|
||||||
|
private operator fun ClassDef.component2() = this.methods
|
@@ -0,0 +1,75 @@
|
|||||||
|
package app.revanced.patcher.signature.implementation.method.resolver
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.softCompareTo
|
||||||
|
import app.revanced.patcher.signature.implementation.method.MethodSignature
|
||||||
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
|
import org.jf.dexlib2.Opcode
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of a [MethodSignatureResolver].
|
||||||
|
* @param definingClassProxy The [ClassProxy] that the matching method was found in.
|
||||||
|
* @param resolvedMethod The actual matching method.
|
||||||
|
* @param scanResult Opcodes pattern scan result.
|
||||||
|
*/
|
||||||
|
data class SignatureResolverResult(
|
||||||
|
val definingClassProxy: ClassProxy,
|
||||||
|
val scanResult: PatternScanResult,
|
||||||
|
private val resolvedMethod: Method,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Returns the **mutable** method by the [resolvedMethod] from the [definingClassProxy].
|
||||||
|
*
|
||||||
|
* Please note, this method allocates a [ClassProxy].
|
||||||
|
* Use [immutableMethod] where possible.
|
||||||
|
*/
|
||||||
|
val method
|
||||||
|
get() = definingClassProxy.resolve().methods.first {
|
||||||
|
it.softCompareTo(resolvedMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the **immutable** method by the [resolvedMethod] from the [definingClassProxy].
|
||||||
|
*
|
||||||
|
* If you need to modify the method, use [method] instead.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
val immutableMethod: Method
|
||||||
|
get() = definingClassProxy.immutableClass.methods.first {
|
||||||
|
it.softCompareTo(resolvedMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findParentMethod(signature: MethodSignature): SignatureResolverResult? {
|
||||||
|
return MethodSignatureResolver.resolveFromProxy(definingClassProxy, signature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PatternScanResult(
|
||||||
|
val startIndex: Int,
|
||||||
|
val endIndex: Int
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* A list of warnings the resolver found.
|
||||||
|
*
|
||||||
|
* This list will be allocated when the signature has been found.
|
||||||
|
* Meaning, if the signature was not found,
|
||||||
|
* or the signature was not yet resolved,
|
||||||
|
* the list will be null.
|
||||||
|
*/
|
||||||
|
var warnings: List<Warning>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a resolver warning.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
data class Warning(
|
||||||
|
val correctOpcode: Opcode,
|
||||||
|
val wrongOpcode: Opcode,
|
||||||
|
val instructionIndex: Int,
|
||||||
|
val patternIndex: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
15
src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt
Normal file
15
src/main/kotlin/app/revanced/patcher/util/ListBackedSet.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
internal class ListBackedSet<E>(private val list: MutableList<E>) : MutableSet<E> {
|
||||||
|
override val size get() = list.size
|
||||||
|
override fun add(element: E) = list.add(element)
|
||||||
|
override fun addAll(elements: Collection<E>) = list.addAll(elements)
|
||||||
|
override fun clear() = list.clear()
|
||||||
|
override fun iterator() = list.listIterator()
|
||||||
|
override fun remove(element: E) = list.remove(element)
|
||||||
|
override fun removeAll(elements: Collection<E>) = list.removeAll(elements)
|
||||||
|
override fun retainAll(elements: Collection<E>) = list.retainAll(elements)
|
||||||
|
override fun contains(element: E) = list.contains(element)
|
||||||
|
override fun containsAll(elements: Collection<E>) = list.containsAll(elements)
|
||||||
|
override fun isEmpty() = list.isEmpty()
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
|
||||||
|
class ProxyBackedClassList(internal val internalClasses: MutableList<ClassDef>) : List<ClassDef> {
|
||||||
|
private val internalProxies = mutableListOf<ClassProxy>()
|
||||||
|
internal val proxies: List<ClassProxy> = internalProxies
|
||||||
|
|
||||||
|
fun add(classDef: ClassDef) = internalClasses.add(classDef)
|
||||||
|
fun add(classProxy: ClassProxy) = internalProxies.add(classProxy)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply all resolved classes into [internalClasses] and clean the [proxies] list.
|
||||||
|
*/
|
||||||
|
internal fun applyProxies() {
|
||||||
|
// FIXME: check if this could cause issues when multiple patches use the same proxy
|
||||||
|
internalProxies.removeIf { proxy ->
|
||||||
|
// if the proxy is unused, keep it in the list
|
||||||
|
if (!proxy.proxyUsed) return@removeIf false
|
||||||
|
|
||||||
|
// if it has been used, replace the internal class which it proxied
|
||||||
|
val index = internalClasses.indexOfFirst { it.type == proxy.immutableClass.type }
|
||||||
|
internalClasses[index] = proxy.mutatedClass
|
||||||
|
|
||||||
|
// return true to remove it from the proxies list
|
||||||
|
return@removeIf true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val size get() = internalClasses.size
|
||||||
|
override fun contains(element: ClassDef) = internalClasses.contains(element)
|
||||||
|
override fun containsAll(elements: Collection<ClassDef>) = internalClasses.containsAll(elements)
|
||||||
|
override fun get(index: Int) = internalClasses[index]
|
||||||
|
override fun indexOf(element: ClassDef) = internalClasses.indexOf(element)
|
||||||
|
override fun isEmpty() = internalClasses.isEmpty()
|
||||||
|
override fun iterator() = internalClasses.iterator()
|
||||||
|
override fun lastIndexOf(element: ClassDef) = internalClasses.lastIndexOf(element)
|
||||||
|
override fun listIterator() = internalClasses.listIterator()
|
||||||
|
override fun listIterator(index: Int) = internalClasses.listIterator(index)
|
||||||
|
override fun subList(fromIndex: Int, toIndex: Int) = internalClasses.subList(fromIndex, toIndex)
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user